Aliquot

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:manage o *:*) 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-Key es obligatorio. Repetir el request con la misma key y el mismo body devuelve la misma invoice (200, header Idempotent-Replay: true); la misma key con otro body es 409 IDEMPOTENCY_KEY_REUSED.
  • merchant y splits referencian cuentas creadas con POST /api/v1/crypto/accounts (kind MERCHANT o PARTNER). Sin merchant, el seller es la propia cuenta PLATFORM de la organization.
  • Omití network y token (y mandá "mode": "test" o "live") para que el comprador elija en el checkout.
  • POST /api/v1/crypto/quote devuelve 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.

En esta página