Inicio rápido
API key → invoice → checkout hosteado → webhook, contra la API de merchant.
Última actualización 27 oct 2026
Todo lo que sigue corre contra un stack local (ver Desarrollo local); cambiá el host por el de tu deployment. API=http://localhost:3005.
1. Una API key
La API de merchant acepta la sesión con cookie del dashboard (más x-organization-id) o una API key de la organization (Authorization: Bearer fk_live_…). El tráfico con API key queda registrado en el log de requests (GET /api/v1/crypto/requests, dashboard → Developers → Logs).
Creá una en el dashboard (Developers → API keys; el secreto se muestra una sola vez) o por la API con tu sesión:
curl -X POST "$API/api/v1/organizations/$ORG_ID/api-keys" \
-H 'content-type: application/json' -H "x-organization-id: $ORG_ID" --cookie "$SESSION" \
-d '{"name":"backend","permissions":["crypto-invoices:create","crypto-invoices:read","webhooks:manage"]}'Los operadores también pueden crear una desde el servidor: bun run --cwd apps/server mint:api-key --organization-id=<id> --email=<owner> --name=backend (permissions por defecto *:*).
[!WARNING] Una key que puede hacer payouts (
crypto-payouts:create,crypto-payouts:manageo*:*) necesita un step-up TOTP al momento de crearse (totp_code, 2FA obligatorio). Ver Payouts.
2. Una invoice
curl -X POST "$API/api/v1/crypto/invoices" \
-H "authorization: Bearer $KEY" -H 'content-type: application/json' \
-H 'idempotency-key: order-77310' \
-d '{
"amount": "100.00", "currency": "USD",
"network": "POLYGON_AMOY", "token": "USDT",
"fee_payer": "CUSTOMER", "markup_percent": 2,
"merchant": "halden_type", "splits": [{ "account": "kerning_weekly", "percent": 10 }],
"title": "Halden Grotesk — Desktop license", "merchant_order_id": "order-77310",
"buyer_email": "[email protected]", "redirect_url": "https://shop.example.com/thanks"
}'Idempotency-Keyes obligatorio. Repetir el request con la misma key y el mismo body devuelve la misma invoice (200, headerIdempotent-Replay: true); la misma key con otro body es409 IDEMPOTENCY_KEY_REUSED.merchantysplitsreferencian cuentas creadas conPOST /api/v1/crypto/accounts(kindMERCHANToPARTNER). Sinmerchant, el seller es la propia cuenta PLATFORM de la organization.- Omití
networkytoken(y mandá"mode": "test"o"live") para que el comprador elija en el checkout. POST /api/v1/crypto/quotedevuelve el mismo desglose sin crear nada.
La respuesta trae id (inv_…), status (PENDING), total ("102.50": 100 + 0.50 % + 2 %), deposit_address, payment_uri (EIP-681), required_confirmations, expires_at y pay_url.
3. El comprador paga
Mandá al comprador a pay_url (<CRYPTO_CHECKOUT_BASE_URL>/pay/<id>, por defecto APP_URL). El checkout hosteado muestra el monto exacto, el código QR y un botón Pay with wallet (EIP-1193: cambia o agrega la cadena, chequea saldos, envía el transfer ERC-20). Avanza solo: PENDING → DETECTED (con un contador de confirmaciones en vivo) → PAID, después el recibo con la barra de split, una invoice imprimible y la vuelta a redirect_url.
Para embeber en vez de redirigir, el cliente incluye <CryptoCheckout /> (apps/client/components/aliquot/embed/, demo en /embed); dentro de Telegram, /pay/:id?tg=1 corre como Mini-App.
4. El webhook
curl -X POST "$API/api/v1/organizations/$ORG_ID/webhooks/endpoints" \
-H 'content-type: application/json' -H "authorization: Bearer $KEY" \
-d '{"url":"https://shop.example.com/hooks/aliquot","events":["invoice.paid","invoice.overpaid","refund.claimable"]}'Registrar un endpoint requiere webhooks:manage. La respuesta incluye el secret del endpoint (se muestra una sola vez). Cada entrega es un POST con el envelope { id, type, occurredAt, data } y los headers X-Webhook-Event, X-Webhook-Timestamp y X-Webhook-Signature: t=<unix>,v1=<hex>, donde v1 = HMAC-SHA256(secret, "<t>.<raw body>"):
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verify(rawBody: string, header: string, secret: string): boolean {
const [, t, v1] = header.match(/t=(\d+),v1=([0-9a-f]+)/) ?? [];
if (!t || !v1) return false;
const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
return v1.length === expected.length && timingSafeEqual(Buffer.from(v1), Buffer.from(expected));
}Eventos cripto: invoice.created, invoice.detected, invoice.paid, invoice.overpaid (se envía junto con invoice.paid), invoice.underpaid, invoice.expired, payout.completed, payout.failed, refund.claimable (lleva claim_url y buyer_email), refund.completed. Los payloads usan snake_case y strings decimales (data.invoice_id, …). Despachá el pedido al recibir invoice.paid.