Pagos P2P para apps
y agentes de IA

Integra envíos de dinero entre usuarios en minutos. API REST con autenticación por API key, webhooks y rate limiting listo para producción.

Quick start

Tres pasos para enviar tu primer pago.

1

Obtén un token JWT

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

Crea una API key (modo desarrollador)

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

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

Envía un pago

# Enviar $100 MXN a un usuario
curl -X POST https://api.payg0.io/api/v1/payments/send \
  -H 'X-API-Key: pyg0_test_xxxxxxxxxxxxx' \
  -H 'Content-Type: application/json' \
  -d '{"recipient": "@carlos", "amount": 100.00}'

# Response
{
  "id": "uuid...",
  "status": "COMPLETED",
  "reason": "transferred",
  "amount": "100.00"
}

Autenticación

Dos métodos según el contexto.

API Key X-API-Key

Ideal para integraciones server-to-server y agentes de IA. Genera tus keys en POST /api/v1/keys.

X-API-Key: pyg0_test_a1b2c3d4e5f6g7h8

JWT Bearer

Para sesiones de usuario. Token de 24 horas obtenido en POST /api/v1/auth/login.

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Sandbox vs Producción: Las API keys de sandbox tienen prefijo pyg0_test_. En producción usarán pyg0_live_. En sandbox el dinero es simulado y no hay movimientos bancarios reales.

Endpoints

Todos los endpoints disponibles en la API.

Auth

MétodoRutaDescripciónRate limit
POST/api/v1/auth/registerCrear cuenta10/min
POST/api/v1/auth/loginObtener JWT10/min
GET/api/v1/auth/mePerfil del usuario autenticado30/min

Wallet

MétodoRutaDescripciónRate limit
GET/api/v1/wallet/balanceSaldo disponible, retenido y total30/min

Pagos

MétodoRutaDescripciónRate limit
POST/api/v1/payments/validateValidar pago sin ejecutarlo (pre-confirmación, límites de tier)5/min
POST/api/v1/payments/sendEnviar pago a nickname o email5/min
GET/api/v1/payments/historyHistorial paginado con filtros30/min
GET/api/v1/payments/{id}Detalle de transacción30/min
POST/api/v1/payments/{id}/cancelCancelar pago PENDING (solo emisor)30/min

Usuarios

MétodoRutaDescripciónRate limit
GET/api/v1/users/lookup?q=Buscar usuario por nickname o email. Devuelve nickname y display_name enmascarado (ej. Carlos L.) — nunca el nombre completo30/min
GET/api/v1/users/{id}/tierTier activo, límites y % de uso del mes (solo propio)30/min
PATCH/api/v1/users/developer-modeActivar/desactivar modo desarrollador30/min
POST/api/v1/users/me/pinCrear PIN de confirmación de pagos10/min
PATCH/api/v1/users/me/pinCambiar PIN10/min
DELETE/api/v1/users/me/pinEliminar PIN10/min
GET/api/v1/users/me/pin/statusEstado del PIN (activo o no)30/min
PATCH/api/v1/users/me/api-keys/{key_id}/allow-withdrawalsHabilitar/deshabilitar retiros para una API key30/min

API Keys (requiere modo dev)

MétodoRutaDescripciónRate limit
POST/api/v1/keysCrear API key (full key solo en este response)30/min
GET/api/v1/keysListar API keys activas30/min
DELETE/api/v1/keys/{id}Revocar API key30/min

Webhooks (requiere modo dev · acepta X-API-Key o Bearer)

MétodoRutaDescripciónRate limit
POST/api/v1/webhooksRegistrar webhook — devuelve secret una única vez, guárdalo10/min
GET/api/v1/webhooksListar webhooks activos (sin secret)30/min
PATCH/api/v1/webhooks/{id}Actualizar URL y/o eventos sin borrar el webhook (mantiene el secret)10/min
POST/api/v1/webhooks/{id}/rotate-secretGenera nuevo secret — invalida el anterior de inmediato, devuelto una única vez10/min
DELETE/api/v1/webhooks/{id}Eliminar webhook30/min

Uso y métricas (requiere modo dev)

MétodoRutaDescripciónRate limit
GET/api/v1/usageConsumo de API: total de calls, breakdown por key y por endpoint20/min

Webhooks

Recibe notificaciones en tiempo real cuando ocurre un evento de pago.

Comportamiento del secret

  • POST /webhooks devuelve el secret una sola vez al registrar — guárdalo de inmediato
  • GET /webhooks no devuelve el secret (solo metadata)
  • POST /webhooks/{id}/rotate-secret genera un nuevo secret, lo devuelve una sola vez e invalida el anterior de inmediato

Paso 1 — Registrar el webhook

Acepta X-API-Key o Bearer — los agentes de IA pueden registrar y gestionar sus webhooks directamente con su API key, sin necesidad de login.

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://tu-app.com/webhooks/payg0",
    "events": ["payment.received", "payment.completed"]
  }'

# Response — guarda el secret ahora, no se vuelve a mostrar
{
  "id": "uuid...",
  "url": "https://tu-app.com/webhooks/payg0",
  "events": ["payment.received", "payment.completed"],
  "secret": "a3f8c2d1e9b4...",
  "is_active": true
}

Paso 2 — Verificar la firma en cada entrega

Cada request incluye el header X-Payg0-Signature: sha256={hmac}. Verifica con el secret recibido al registrar.

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)

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

Paso 3 — Actualizar eventos sin borrar el webhook

Puedes cambiar la URL o los eventos suscritos en cualquier momento. El secret y el ID permanecen igual.

# Suscribirse solo a pagos recibidos (quitar 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"]}'

# También puedes actualizar solo la 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://nueva-url.com/webhook"}'

Paso 4 — Si pierdes el secret, rótalo

No es necesario borrar y volver a registrar el webhook. Usa este endpoint — el secret anterior queda inválido de inmediato.

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

# Response — nuevo secret, devuelto una única vez
{
  "id": "uuid...",
  "secret": "b7e1a4f2c9d8..."
}

Estructura del payload

Cada entrega es un POST con Content-Type: application/json y este cuerpo:

{
  "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":     "Es otra prueba",
    "created_at":      "2026-05-26T17:35:00+00:00",
    "updated_at":      "2026-05-26T17:35:00+00:00"
  }
}

Eventos disponibles

EventoCuándo se disparaEn la cuenta de
payment.completedPago completado exitosamenteEmisor
payment.receivedLlegó un pago completadoReceptor
payment.pendingPago en espera de que el receptor se registreEmisor
payment.cancelledPago cancelado por el emisorEmisor
payment.failedPago fallidoEmisor
payment.expiredPago pendiente expirado sin ser reclamadoEmisor

Ejemplos de código

Enviar un pago en distintos lenguajes.

Python
JavaScript
cURL
import httpx

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

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

# Enviar pago
response = client.post("/api/v1/payments/send", json={
    "recipient": "@carlos",
    "amount": 150.00,
    "description": "Pago por servicio",
})
tx = response.json()
print(f"Transacción {tx['id']}: {tx['status']}")
const BASE = "https://api.payg0.io";
const HEADERS = {
  "X-API-Key": "pyg0_test_xxxxxxxxxxxxx",
  "Content-Type": "application/json",
};

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

// Enviar pago
const tx = await fetch(`${BASE}/api/v1/payments/send`, {
  method: "POST",
  headers: HEADERS,
  body: JSON.stringify({ recipient: "@carlos", amount: 150 }),
}).then(r => r.json());
console.log(`Transacción ${tx.id}: ${tx.status}`);
# Saldo
curl https://api.payg0.io/api/v1/wallet/balance \
  -H "X-API-Key: pyg0_test_xxxxxxxxxxxxx"

# Enviar pago
curl -X POST https://api.payg0.io/api/v1/payments/send \
  -H "X-API-Key: pyg0_test_xxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{"recipient":"@carlos","amount":150,"description":"Pago"}'

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

Rate limits

Los límites se aplican por API key, sesión JWT o IP.

OperaciónLímite
POST /api/v1/payments/send5 por minuto
GET endpoints en general30 por minuto
POST /api/v1/webhooks10 por minuto
GET /api/v1/usage20 por minuto
Auth (login / register)10 por minuto

Al superar el límite recibirás 429 Too Many Requests. El header Retry-After indica cuándo reintentar.

Códigos de error

Todos los errores retornan JSON con el campo detail.

CódigoSignificado
400Bad Request — parámetros inválidos
401Unauthorized — token o API key inválida o expirada
403Forbidden — sin permisos (ej. modo dev no activo)
404Not Found — recurso no encontrado
409Conflict — email o nickname ya registrado
422Unprocessable — fondos insuficientes, datos inválidos o límite de tier excedido (LIMIT_SINGLE_TX, LIMIT_BALANCE, LIMIT_MONTHLY)
429Too Many Requests — rate limit excedido
500Internal Server Error — error interno inesperado