Add person-to-person money transfers in minutes. A REST API with API key authentication, webhooks and production-ready rate limiting.
Three steps to send your first payment.
# 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"}'
# 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"}'
# 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.
Two methods, depending on the context.
X-API-KeyBest for server-to-server integrations and AI agents. Create your keys with POST /api/v1/keys.
X-API-Key: pyg0_test_a1b2c3d4e5f6g7h8
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.
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.
| Method | Path | Description | Rate limit |
|---|---|---|---|
| POST | /api/v1/auth/login | Logs in with email and password and returns a JWT for user sessions (24 hours). | 10/min |
| GET | /api/v1/auth/me | Profile of the authenticated user. | — |
| Method | Path | Description | Rate limit |
|---|---|---|---|
| GET | /api/v1/wallet/balance | Total, held and available balance. | 30/min |
| Method | Path | Description | Rate limit |
|---|---|---|---|
| POST | /api/v1/payments/intents | Proposes 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 |
| Method | Path | Description | Rate limit |
|---|---|---|---|
| POST | /api/v1/payments/validate | Validates a payment WITHOUT running it. | 5/min |
| POST | /api/v1/payments/send | Sends a payment without human confirmation. | 5/min |
| GET | /api/v1/payments/history | Sent 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}/cancel | Cancels a PENDING payment (only the sender can). | 30/min |
| Method | Path | Description | Rate limit |
|---|---|---|---|
| GET | /api/v1/users/lookup | Finds a user by nickname or email. | 30/min |
| PATCH | /api/v1/users/developer-mode | Turns developer mode on or off (needed to create API keys and webhooks). | — |
| POST | /api/v1/users/me/pin | Sets up the payment PIN of an account without one. | 10/min |
| PATCH | /api/v1/users/me/pin | Changes the payment PIN. | 10/min |
| DELETE | /api/v1/users/me/pin | Removes the payment PIN. | 10/min |
| GET | /api/v1/users/me/pin/status | Whether the user has an active PIN and whether there is a lockout. | — |
| GET | /api/v1/users/{user_id}/tier | Returns the tier, limits and usage percentage for the current month. | 30/min |
| Method | Path | Description | Rate limit |
|---|---|---|---|
| POST | /api/v1/keys | Creates an API key. | 30/min |
| GET | /api/v1/keys | Lists 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/webhooks | Registers a webhook. | 10/min |
| GET | /api/v1/webhooks | Lists 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-secret | Generates a new secret for the webhook. | 10/min |
| GET | /api/v1/usage | API usage: total calls, broken down by key and by endpoint. | 20/min |
For agents and apps that must pay without a person present.
POST /api/v1/payments/send without a PIN. The API never accepts a PIN.403 AUTOPAY_OFF / AUTOPAY_EXPIRED / PAYMENTS_ON_HOLD; 422 AUTOPAY_LIMIT_PAYMENT / AUTOPAY_LIMIT_DAILY, plus the tier limit errors.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 awayGET /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 immediatelyAccepts 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
}
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)
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"}'
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..."
}
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"
}
}
| Event | When it fires | Delivered to |
|---|---|---|
payment.completed | Payment completed successfully | Sender |
payment.received | A completed payment arrived | Receiver |
payment.pending | Payment waiting for the receiver to sign up (up to 3 days) | Sender |
payment.cancelled | Payment cancelled by the sender | Sender |
payment.failed | Payment failed | Sender |
payment.expired | Pending payment not claimed within 3 days; the money returns to the sender | Sender |
Sending a payment in several languages.
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"
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.
Every error returns JSON with a detail field.
| Code | Meaning |
|---|---|
| 400 | Bad Request: invalid parameters |
| 401 | Unauthorized: invalid or expired token or API key |
| 403 | Forbidden: no permission (e.g. developer mode is off) |
| 404 | Not Found: resource not found |
| 422 | Unprocessable: insufficient funds, invalid data or tier limit exceeded (LIMIT_SINGLE_TX, LIMIT_BALANCE, LIMIT_MONTHLY) |
| 429 | Too Many Requests: rate limit exceeded |
| 500 | Internal Server Error: unexpected internal error |