For developers For AI agents

P2P payments for apps
and AI agents

Add person-to-person money transfers in minutes. A REST API with API key authentication, webhooks and production-ready rate limiting.

Quick start

Three steps to send your first payment.

1

Get a JWT token

# POST /api/v1/auth/login
curl -X POST https://api.payg0.io/api/v1/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"email":"you@email.com","password":"your-password"}'
2

Create an API key (developer mode)

# Turn on developer mode first
curl -X PATCH https://api.payg0.io/api/v1/users/developer-mode \
  -H 'Authorization: Bearer {token}' \
  -H 'Content-Type: application/json' \
  -d '{"enabled": true}'

# Create your API key
curl -X POST https://api.payg0.io/api/v1/keys \
  -H 'Authorization: Bearer {token}' \
  -H 'Content-Type: application/json' \
  -d '{"name": "My app"}'
3

Send a payment

# Prepare $100 MXN for @carlos: no money moves yet
curl -X POST https://api.payg0.io/api/v1/payments/intents \
  -H 'X-API-Key: pyg0_test_xxxxxxxxxxxxx' \
  -H 'Content-Type: application/json' \
  -d '{"recipient": "@carlos", "amount": "100.00"}'

# Response: the user opens confirm_url and confirms with their PIN on payg0.io
{
  "id": "uuid...",
  "status": "AWAITING_CONFIRMATION",
  "confirm_url": "https://payg0.io/confirm-payment/uuid..."
}

# Then check it: CONFIRMED, DECLINED, EXPIRED or FAILED
curl https://api.payg0.io/api/v1/payments/intents/{id} \
  -H 'X-API-Key: pyg0_test_xxxxxxxxxxxxx'

The API never asks for the user's PIN. To send without confirmation, see automatic payments.

Authentication

Two methods, depending on the context.

API Key X-API-Key

Best for server-to-server integrations and AI agents. Create your keys with POST /api/v1/keys.

X-API-Key: pyg0_test_a1b2c3d4e5f6g7h8

JWT Bearer

For user sessions. A 24-hour token from POST /api/v1/auth/login.

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Sandbox vs production: Sandbox API keys start with pyg0_test_. Production keys will start with pyg0_live_. In the sandbox the money is simulated and no real bank transfers happen.

Endpoints

Every endpoint, generated from the code. Base URL https://api.payg0.io; full schemas in openapi.json.

Accounts cannot be created through the API. People sign up at payg0.io/register (email verification included); the API works on accounts that already exist.

Auth

MethodPathDescriptionRate limit
POST/api/v1/auth/loginLogs in with email and password and returns a JWT for user sessions (24 hours).10/min
GET/api/v1/auth/meProfile of the authenticated user.—

Wallet

MethodPathDescriptionRate limit
GET/api/v1/wallet/balanceTotal, held and available balance.30/min

Payment intents

MethodPathDescriptionRate limit
POST/api/v1/payments/intentsProposes a payment that the user must confirm with their PIN at confirm_url.5/min
GET/api/v1/payments/intents/{intent_id}Status of a payment awaiting confirmation.30/min

Payments

MethodPathDescriptionRate limit
POST/api/v1/payments/validateValidates a payment WITHOUT running it.5/min
POST/api/v1/payments/sendSends a payment without human confirmation.5/min
GET/api/v1/payments/historySent and received payments, newest first, filterable by status and dates.30/min
GET/api/v1/payments/{transaction_id}One payment the user sent or received.30/min
POST/api/v1/payments/{transaction_id}/cancelCancels a PENDING payment (only the sender can).30/min

Users

MethodPathDescriptionRate limit
GET/api/v1/users/lookupFinds a user by nickname or email.30/min
PATCH/api/v1/users/developer-modeTurns developer mode on or off (needed to create API keys and webhooks).—
POST/api/v1/users/me/pinSets up the payment PIN of an account without one.10/min
PATCH/api/v1/users/me/pinChanges the payment PIN.10/min
DELETE/api/v1/users/me/pinRemoves the payment PIN.10/min
GET/api/v1/users/me/pin/statusWhether the user has an active PIN and whether there is a lockout.—
GET/api/v1/users/{user_id}/tierReturns the tier, limits and usage percentage for the current month.30/min

API keys, webhooks and usage

MethodPathDescriptionRate limit
POST/api/v1/keysCreates an API key.30/min
GET/api/v1/keysLists the user's active API keys (prefix and name only).30/min
DELETE/api/v1/keys/{key_id}Revokes an API key immediately.30/min
POST/api/v1/webhooksRegisters a webhook.10/min
GET/api/v1/webhooksLists active webhooks (without their secrets).30/min
PATCH/api/v1/webhooks/{webhook_id}Updates a webhook's URL and/or events without deleting and recreating it.10/min
DELETE/api/v1/webhooks/{webhook_id}Deletes a webhook.—
POST/api/v1/webhooks/{webhook_id}/rotate-secretGenerates a new secret for the webhook.10/min
GET/api/v1/usageAPI usage: total calls, broken down by key and by endpoint.20/min

Automatic payments

For agents and apps that must pay without a person present.

Webhooks

Get real-time notifications when a payment event happens.

How the secret works

  • → POST /webhooks returns the secret only once when you register; store it right away
  • → GET /webhooks does not return the secret (metadata only)
  • → POST /webhooks/{id}/rotate-secret creates a new secret, returns it only once and invalidates the old one immediately

Step 1: Register the webhook

Accepts X-API-Key or Bearer: AI agents can register and manage their webhooks directly with their API key, without logging in.

curl -X POST https://api.payg0.io/api/v1/webhooks \
  -H 'X-API-Key: pyg0_test_xxxx' \
  -H 'Content-Type: application/json' \
  -d '{
    "url": "https://your-app.com/webhooks/payg0",
    "events": ["payment.received", "payment.completed"]
  }'

# Response: store the secret now, it is never shown again
{
  "id": "uuid...",
  "url": "https://your-app.com/webhooks/payg0",
  "events": ["payment.received", "payment.completed"],
  "secret": "a3f8c2d1e9b4...",
  "is_active": true
}

Step 2: Verify the signature on every delivery

Every request includes the header X-Payg0-Signature: sha256={hmac}. Verify it with the secret you got when registering.

import hmac, hashlib

def verify_signature(payload: bytes, secret: str, signature: str) -> bool:
    expected = "sha256=" + hmac.new(
        secret.encode(), payload, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)

# In your handler:
body = await request.body()
sig = request.headers["X-Payg0-Signature"]
if not verify_signature(body, WEBHOOK_SECRET, sig):
    raise HTTPException(401)

Step 3: Update events without deleting the webhook

You can change the URL or the subscribed events at any time. The secret and the ID stay the same.

# Subscribe only to received payments (drop payment.completed)
curl -X PATCH https://api.payg0.io/api/v1/webhooks/{id} \
  -H 'X-API-Key: pyg0_test_xxxx' \
  -H 'Content-Type: application/json' \
  -d '{"events": ["payment.received"]}'

# You can also update only the URL
curl -X PATCH https://api.payg0.io/api/v1/webhooks/{id} \
  -H 'X-API-Key: pyg0_test_xxxx' \
  -H 'Content-Type: application/json' \
  -d '{"url": "https://new-url.com/webhook"}'

Step 4: If you lose the secret, rotate it

No need to delete and register the webhook again. Use this endpoint; the old secret stops working immediately.

curl -X POST https://api.payg0.io/api/v1/webhooks/{id}/rotate-secret \
  -H 'X-API-Key: pyg0_test_xxxx'

# Response: the new secret, returned only once
{
  "id": "uuid...",
  "secret": "b7e1a4f2c9d8..."
}

Payload structure

Each delivery is a POST with Content-Type: application/json and this body:

{
  "event": "payment.received",
  "data": {
    "id": "bc1b2680-d870-41fd-8063-d79bd749d724",
    "sender_id":       "808e5be1-12d7-4a58-ad56-f3d6a2a8fd38",
    "sender_nickname":  "nando",
    "receiver_id":      "ff793bad-7d97-4e5e-baba-6ecc70c12260",
    "receiver_nickname": "diegotco",
    "receiver_email":   null,
    "amount":          "2.97",
    "currency":        "MXN",
    "status":          "COMPLETED",
    "reason":          "transferred",
    "description":     "Another test",
    "created_at":      "2026-05-26T17:35:00+00:00",
    "updated_at":      "2026-05-26T17:35:00+00:00"
  }
}

Available events

EventWhen it firesDelivered to
payment.completedPayment completed successfullySender
payment.receivedA completed payment arrivedReceiver
payment.pendingPayment waiting for the receiver to sign up (up to 3 days)Sender
payment.cancelledPayment cancelled by the senderSender
payment.failedPayment failedSender
payment.expiredPending payment not claimed within 3 days; the money returns to the senderSender

Code examples

Sending a payment in several languages.

Python
JavaScript
cURL
import httpx

client = httpx.Client(
    base_url="https://api.payg0.io",
    headers={"X-API-Key": "pyg0_test_xxxxxxxxxxxxx"},
)

# Check the balance
balance = client.get("/api/v1/wallet/balance").json()
print(f"Available: ${balance['available_balance']} MXN")

# Prepare a payment; the user confirms it with their PIN on payg0.io
intent = client.post("/api/v1/payments/intents", json={
    "recipient": "@carlos",
    "amount": "150.00",
    "description": "Payment for services",
}).json()
print(f"Confirm here: {intent['confirm_url']}")
const BASE = "https://api.payg0.io";
const HEADERS = {
  "X-API-Key": "pyg0_test_xxxxxxxxxxxxx",
  "Content-Type": "application/json",
};

// Check the balance
const balance = await fetch(`${BASE}/api/v1/wallet/balance`, { headers: HEADERS })
  .then(r => r.json());
console.log(`Available: $${balance.available_balance} MXN`);

// Prepare a payment; the user confirms it with their PIN on payg0.io
const intent = await fetch(`${BASE}/api/v1/payments/intents`, {
  method: "POST",
  headers: HEADERS,
  body: JSON.stringify({ recipient: "@carlos", amount: "150.00" }),
}).then(r => r.json());
console.log(`Confirm here: ${intent.confirm_url}`);
# Balance
curl https://api.payg0.io/api/v1/wallet/balance \
  -H "X-API-Key: pyg0_test_xxxxxxxxxxxxx"

# Prepare a payment (the user confirms it with their PIN on payg0.io)
curl -X POST https://api.payg0.io/api/v1/payments/intents \
  -H "X-API-Key: pyg0_test_xxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{"recipient":"@carlos","amount":"150.00","description":"Payment"}'

# History
curl "https://api.payg0.io/api/v1/payments/history?status=COMPLETED&limit=10" \
  -H "X-API-Key: pyg0_test_xxxxxxxxxxxxx"

Rate limits

Each endpoint's limit is in the tables above. Limits apply per API key, JWT session or IP.

Above the limit you get 429 Too Many Requests. Limits are per minute: wait about a minute before retrying.

Error codes

Every error returns JSON with a detail field.

CodeMeaning
400Bad Request: invalid parameters
401Unauthorized: invalid or expired token or API key
403Forbidden: no permission (e.g. developer mode is off)
404Not Found: resource not found
422Unprocessable: insufficient funds, invalid data or tier limit exceeded (LIMIT_SINGLE_TX, LIMIT_BALANCE, LIMIT_MONTHLY)
429Too Many Requests: rate limit exceeded
500Internal Server Error: unexpected internal error