diff --git a/docs.json b/docs.json index 39de405..44cd372 100644 --- a/docs.json +++ b/docs.json @@ -48,6 +48,17 @@ "es/guides/postman-collections" ] }, + { + "group": "Sandbox", + "pages": [ + "es/sandbox/introduction", + "es/sandbox/authentication", + "es/sandbox/scenarios", + "es/sandbox/cash-in", + "es/sandbox/cash-out", + "es/sandbox/webhooks" + ] + }, { "group": "Webhooks", "pages": [ @@ -92,6 +103,17 @@ "en/guides/postman-collections" ] }, + { + "group": "Sandbox", + "pages": [ + "en/sandbox/introduction", + "en/sandbox/authentication", + "en/sandbox/scenarios", + "en/sandbox/cash-in", + "en/sandbox/cash-out", + "en/sandbox/webhooks" + ] + }, { "group": "Webhooks", "pages": [ @@ -138,6 +160,17 @@ "pt-br/guides/postman-collections" ] }, + { + "group": "Sandbox", + "pages": [ + "pt-br/sandbox/introduction", + "pt-br/sandbox/authentication", + "pt-br/sandbox/scenarios", + "pt-br/sandbox/cash-in", + "pt-br/sandbox/cash-out", + "pt-br/sandbox/webhooks" + ] + }, { "group": "Eventos de Webhook", "pages": [ diff --git a/en/sandbox/authentication.mdx b/en/sandbox/authentication.mdx new file mode 100644 index 0000000..f123c80 --- /dev/null +++ b/en/sandbox/authentication.mdx @@ -0,0 +1,81 @@ +--- +title: 'Authentication' +description: 'Authentication in sandbox is structurally identical to production.' +mode: 'wide' +--- + +## Overview + +Sandbox authentication uses the same two layers as production: + +1. **X.509 certificate (mTLS)** — issued by NTX Pay at onboarding. +2. **OAuth 2.0 `client_credentials`** — `clientId` + `clientSecret` received during signup. + +Together they return a **JWT** (10-minute validity) used on the remaining endpoints as `Authorization: Bearer ...`. + + + Sandbox credentials are **distinct** from production. If you use production credentials against `https://sandbox.ntxpay.com`, you will get `401`. The HTTP contract is identical — what changes is the certificate + clientId/clientSecret pair. + + +## Get a Token + +### POST /api/auth/token + +```bash +curl -X POST https://sandbox.ntxpay.com/api/auth/token \ + -H "X-SSL-Client-Cert: $ENCODED_CERT" \ + -H "Content-Type: application/json" \ + -d '{ + "clientId": "qr-93-550e8400", + "clientSecret": "a1b2c3d4e5f6g7h8" + }' +``` + +#### Response (201) + +```json +{ + "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "token_type": "Bearer", + "expires_in": 600, + "scope": "email profile" +} +``` + +## Use the Token + +On a sandbox account, any authenticated call simulates the full pipeline without moving real money: + +```bash +curl -X POST https://sandbox.ntxpay.com/api/spei/cash-out \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "amountCentavos": 15000, + "destinationClabe": "012180001234567890", + "beneficiaryName": "Maria Lopez", + "externalId": "test-001" + }' +``` + +The response is always `201 Created` with `status: PENDING`. The final outcome (confirmation or failure) arrives via webhook ~1 second later. See [Scenarios](/en/sandbox/scenarios) to force specific outcomes. + +## Renewal + +The token expires in **10 minutes (600s)**. There is no refresh token — get a new one via `POST /api/auth/token` before expiration. + + + Under high load, mint a token per worker and renew every ~8 minutes to avoid `401` due to expiration. + + +## Common Errors + +| Code | Cause | Fix | +|---|---|---| +| `400` | `X-SSL-Client-Cert` missing | Configure NGINX/ALB to forward the certificate | +| `401` | Invalid `clientId`/`clientSecret` | Double-check credentials; confirm you are using the sandbox ones | +| `401` | Certificate expired/revoked | Request renewal from NTX Pay | + +## Detailed documentation + +For the full step-by-step (certificate encoding, examples in multiple languages, etc.) see [Authentication](/en/guides/authentication) in the main guide — the only difference is the base URL `https://sandbox.ntxpay.com`. diff --git a/en/sandbox/cash-in.mdx b/en/sandbox/cash-in.mdx new file mode 100644 index 0000000..a55bd82 --- /dev/null +++ b/en/sandbox/cash-in.mdx @@ -0,0 +1,88 @@ +--- +title: 'Cash-in (SPEI receive)' +description: 'How to generate a disposable cash-in CLABE in sandbox.' +mode: 'wide' +--- + +## What it does + +`POST /api/spei/cash-in` generates a **disposable CLABE** bound to your sandbox account. Any SPEI transfer received at that CLABE triggers a `cash_in` webhook to the configured URL. + +In sandbox, confirmation is **simulated** ~1 second after CLABE creation (instead of waiting for a real transfer). This lets you test the entire cash-in flow without depending on a real issuing bank. + +## Example + +```bash +curl -X POST https://sandbox.ntxpay.com/api/spei/cash-in \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "amountCentavos": 50000, + "externalId": "order-001", + "customerName": "Juan Pérez" + }' +``` + +### Response (201) + +```json +{ + "id": 12345, + "externalId": "order-001", + "status": "PENDING", + "amountCentavos": 50000, + "clabe": "646180123456789012", + "expiresAt": "2026-03-26T10:30:00.000Z" +} +``` + +Use the returned `clabe` to display to the end payer (your company's customer). In sandbox this CLABE is fictitious, but the `transaction.clabe` field arriving in the webhook will be **the same**. + +## Expected webhook + +After ~1 second (default `success` scenario): + +```json +{ + "event": "cash_in", + "deliveryId": "...", + "transaction": { + "id": 12345, + "externalId": "order-001", + "status": "CONFIRMED", + "amountCentavos": 50000, + "clabe": "646180123456789012", + "confirmedAt": "2026-03-26T10:00:01.000Z", + "counterpart": { + "name": "Simulated Payer", + "taxId": "PAGS850101ABC", + "bank": { + "code": "012", + "name": "BBVA México" + } + } + } +} +``` + +## Test scenarios + +| Scenario | Webhook | +|---|---| +| Default | `CONFIRMED` | +| `error:invalid-clabe` | `FAILED` | +| `error:duplicate-external-id` | `FAILED` | +| `delayed:5s` | `CONFIRMED` after 5s | + +See [Scenarios](/en/sandbox/scenarios) for the full list. + +## Next steps + + + + How to send SPEI in sandbox. + + + Understanding webhook delivery in sandbox. + + diff --git a/en/sandbox/cash-out.mdx b/en/sandbox/cash-out.mdx new file mode 100644 index 0000000..841e545 --- /dev/null +++ b/en/sandbox/cash-out.mdx @@ -0,0 +1,102 @@ +--- +title: 'Cash-out (SPEI send)' +description: 'How to send SPEI to a destination CLABE in sandbox.' +mode: 'wide' +--- + +## What it does + +`POST /api/spei/cash-out` requests a SPEI transfer to a destination CLABE. In sandbox, the full accounting pipeline runs — balance is debited, fee is charged, statement entry is generated — but the Banxico call is simulated. + +The HTTP response is always `201 Created` with `status: PENDING`. The final outcome arrives via `cash_out` webhook ~1 second later (`success` scenario) or as forced by the scenario. + +## Prerequisite + +Your sandbox account needs balance. Run at least one [cash-in](/en/sandbox/cash-in) first — simulated balance is debited just like in production. + +## Example + +```bash +curl -X POST https://sandbox.ntxpay.com/api/spei/cash-out \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "amountCentavos": 15000, + "destinationClabe": "012180001234567890", + "beneficiaryName": "Maria Lopez", + "beneficiaryTaxId": "LOPM850101ABC", + "externalId": "payout-001", + "description": "Supplier payment" + }' +``` + +### Response (201) + +```json +{ + "id": 12346, + "externalId": "payout-001", + "status": "PENDING", + "amountCentavos": 15000, + "clabe": "012180001234567890" +} +``` + +## Expected webhook + +After ~1 second (`success` scenario): + +```json +{ + "event": "cash_out", + "deliveryId": "...", + "transaction": { + "id": 12346, + "externalId": "payout-001", + "status": "CONFIRMED", + "amountCentavos": 15000, + "clabe": "012180001234567890", + "referenceNumerical": "9876543", + "confirmedAt": "2026-03-26T10:00:01.000Z" + }, + "errorCode": null, + "errorMessage": null +} +``` + +## Useful error scenarios + +Force specific behaviors via the `X-Sandbox-Scenario` header: + +```bash +curl -X POST https://sandbox.ntxpay.com/api/spei/cash-out \ + -H "Authorization: Bearer $TOKEN" \ + -H "X-Sandbox-Scenario: error:insufficient-funds" \ + -H "Content-Type: application/json" \ + -d '{ ... }' +``` + +| Scenario | When to use | +|---|---| +| `error:insufficient-funds` | Validate UX when your customer tries to pay without balance | +| `error:invalid-clabe` | Validate CLABE parsing/validation in your frontend | +| `error:account-not-found` | Validate post-network error (CLABE exists locally but not on the SPEI network) | +| `error:bank-rejected` | Validate generic fallback | +| `delayed:30s` | Validate "transfer in progress" UX | + +See [Scenarios](/en/sandbox/scenarios) for the full list. + +## Synchronous validations + +Even in sandbox, some validations happen **before** the `201` is returned: + +| Synchronous error | HTTP | When | +|---|---|---| +| `400 INVALID_AMOUNT` | `400` | `amountCentavos <= 0` | +| `400 INVALID_CLABE_FORMAT` | `400` | CLABE with invalid format (non-numeric, wrong length) | +| `400 DUPLICATE_EXTERNAL_ID` | `400` | `externalId` already used in another transaction for this account | +| `400 INSUFFICIENT_FUNDS` | `400` | Real balance below `amountCentavos + fee` (without using a scenario) | + + + Scenarios prefixed with `error:` affect the **webhook** — the synchronous response is always `201 PENDING`. Structural validations like format/duplication fail synchronously. + diff --git a/en/sandbox/introduction.mdx b/en/sandbox/introduction.mdx new file mode 100644 index 0000000..16620a2 --- /dev/null +++ b/en/sandbox/introduction.mdx @@ -0,0 +1,68 @@ +--- +title: 'NTX Pay Sandbox' +description: 'Test environment with high fidelity to the NTX Pay México production pipeline.' +mode: 'wide' +--- + +## What it is + +The NTX Pay sandbox lets your integration exercise **cash-in**, **cash-out**, **refund**, and **webhooks** without moving real money. Unlike simplistic mocks, the full accounting pipeline (TigerBeetle balance, limit validation, fee charging, statement generation, webhook delivery via outbox) is exercised intact. Only the external provider (SPEI/Banxico) is simulated. + + + Every NTX Pay integration starts in sandbox. The endpoints, payloads, and webhooks described in this documentation are the final ones — when production is enabled for your company, the same code works by just swapping credentials. + + +## How to enable + +Your API credentials are **structurally the same** as those you would use in production. The difference lives at the account level: accounts with `mainProvider: "sandbox"` route every SPEI call internally to the NTX simulator. To create a sandbox account, contact your Account Manager or write to `contact@ntxpay.com` — onboarding is instant and KYC is auto-approved. + +## Base URL + +| Environment | URL | +|---|---| +| Sandbox | `https://sandbox.ntxpay.com` | + +All documented routes (`/api/auth/token`, `/api/spei/cash-in`, `/api/spei/cash-out`, `/api/oxxo/cash-in`, `/api/transactions`, `/api/webhooks-config`) are available exactly at this host. + +## Test scenarios + +You control the behavior of each call via the `X-Sandbox-Scenario` HTTP header. Without the header, the sandbox returns **success** by default. See [Scenarios](/en/sandbox/scenarios) for the full list of supported error, success, and delay scenarios. + +## Webhooks + +Register your `webhookUrl` on the sandbox account exactly as you would in production — via `POST /api/webhooks-config`. Events are delivered by the same outbox engine we use in prod, with the same signatures, headers (`X-NTXPay-Delivery`), and retry policy. + +## Differences vs Production + +| Aspect | Sandbox | Production | +|---|---|---| +| Base URL | `https://sandbox.ntxpay.com` | `https://api.ntxpay.com` | +| Provider | `sandbox` (simulated) | Real bank (Banxico/SPEI) | +| Balance | Simulated | Real funds | +| SPEI cash-in confirmation | Immediate (~1s) | Real (seconds to minutes) | +| `X-Sandbox-Scenario` | Supported | Rejected with `400` | +| Cost | Free | Per contract | + +## Next steps + + + + How to obtain the JWT in sandbox using your credentials. + + + Full list of scenarios available via `X-Sandbox-Scenario`. + + + Receive via SPEI in sandbox. + + + Send via SPEI in sandbox. + + + How the sandbox delivers webhooks and how to test dedupe. + + + +## Support + +`support@ntxpay.com` diff --git a/en/sandbox/scenarios.mdx b/en/sandbox/scenarios.mdx new file mode 100644 index 0000000..53b062a --- /dev/null +++ b/en/sandbox/scenarios.mdx @@ -0,0 +1,141 @@ +--- +title: 'Test scenarios' +description: 'Force specific behaviors via the X-Sandbox-Scenario header.' +mode: 'wide' +--- + +## How to use + +Add the header `X-Sandbox-Scenario: ` to any cash-in, cash-out, or OXXO call. Without the header, the sandbox uses the `success` scenario by default. + +```bash +curl -X POST https://sandbox.ntxpay.com/api/spei/cash-out \ + -H "Authorization: Bearer $TOKEN" \ + -H "X-Sandbox-Scenario: error:insufficient-funds" \ + -H "Content-Type: application/json" \ + -d '{ + "amountCentavos": 15000, + "destinationClabe": "012180001234567890", + "beneficiaryName": "Maria Lopez", + "externalId": "test-error-001" + }' +``` + + + The header only controls the **asynchronous webhook**. The HTTP response is always `201 Created` with `status: PENDING`, regardless of the scenario. The final outcome (`CONFIRMED`, `FAILED`, or `EXPIRED`) arrives in the webhook ~1 second later. + + +## Available scenarios + +### Success scenarios + +| Header Value | Synchronous behavior | Webhook | +|---|---|---| +| `success` (default) | `201 PENDING` | `CONFIRMED` in ~1s | +| _(no header)_ | `201 PENDING` | `CONFIRMED` in ~1s | + +### Error scenarios + +| Header Value | Synchronous behavior | Webhook | +|---|---|---| +| `error:insufficient-funds` | `201 PENDING` | `FAILED` with `errorCode: INSUFFICIENT_FUNDS` | +| `error:invalid-clabe` | `201 PENDING` | `FAILED` with `errorCode: INVALID_CLABE` | +| `error:account-not-found` | `201 PENDING` | `FAILED` with `errorCode: ACCOUNT_NOT_FOUND` | +| `error:account-blocked` | `201 PENDING` | `FAILED` with `errorCode: ACCOUNT_BLOCKED` | +| `error:duplicate-external-id` | `201 PENDING` | `FAILED` with `errorCode: DUPLICATE_EXTERNAL_ID` | +| `error:bank-rejected` | `201 PENDING` | `FAILED` with `errorCode: BANK_REJECTED` | +| `error:oxxo-expired` | `201 PENDING` | `EXPIRED` (OXXO only) | + +### Delay scenarios + +| Header Value | Synchronous behavior | Webhook | +|---|---|---| +| `delayed:5s` | `201 PENDING` | `CONFIRMED` after +5s | +| `delayed:30s` | `201 PENDING` | `CONFIRMED` after +30s | +| `delayed:60s` | `201 PENDING` | `CONFIRMED` after +60s | + + + The maximum allowed delay is **120 seconds** — higher values are truncated automatically. + + +## Example: success webhook + +```json +{ + "event": "cash_out", + "deliveryId": "8e2c5b6f-3a12-4b9c-9a18-77a2b3c4d5e6", + "createdAt": "2026-03-26T10:00:00.000Z", + "transaction": { + "id": 12345, + "externalId": "test-success-001", + "paymentMethod": "SPEI", + "direction": "out", + "type": "cash_out", + "status": "CONFIRMED", + "provider": "sandbox", + "amountCentavos": 15000, + "clabe": "012180001234567890", + "referenceNumerical": "9876543", + "createdAt": "2026-03-26T09:59:59.000Z", + "confirmedAt": "2026-03-26T10:00:00.000Z", + "counterpart": { + "name": "Maria Lopez", + "taxId": null, + "bank": {} + } + }, + "errorCode": null, + "errorMessage": null, + "metadata": {} +} +``` + +## Example: failure webhook + +```json +{ + "event": "cash_out", + "deliveryId": "1a3f9e8d-2c47-4b9c-aa18-77a2b3c4d5e6", + "createdAt": "2026-03-26T10:01:00.000Z", + "transaction": { + "id": 12346, + "externalId": "test-error-001", + "paymentMethod": "SPEI", + "direction": "out", + "type": "cash_out", + "status": "FAILED", + "provider": "sandbox", + "amountCentavos": 15000, + "clabe": "012180001234567890", + "referenceNumerical": null, + "confirmedAt": null + }, + "errorCode": "INSUFFICIENT_FUNDS", + "errorMessage": "Account without sufficient balance", + "metadata": {} +} +``` + +Notes: + +- On `status: FAILED`, `referenceNumerical` and `confirmedAt` are `null` — the SPEI network never confirmed the transaction. +- `errorCode` and `errorMessage` describe the reason for the failure. + +## Restrictions + +- The `X-Sandbox-Scenario` header works **exclusively** on sandbox accounts. +- Production accounts sending the header receive: + +```json +{ + "statusCode": 400, + "message": "X-Sandbox-Scenario header is only supported in sandbox mode." +} +``` + +## Best practices + +1. **Test every scenario** before going live — implement handling for `CONFIRMED`, `PENDING`, `FAILED`, and `EXPIRED`. +2. **Validate the error fields** — use `errorCode` for automated decisions; keep `errorMessage` for logs/users. +3. **Test with delay** — make sure your system handles slow webhook delivery well. +4. **Idempotency** — use `transaction.id` as the idempotency key; the same webhook can be re-delivered. diff --git a/en/sandbox/webhooks.mdx b/en/sandbox/webhooks.mdx new file mode 100644 index 0000000..5ea6507 --- /dev/null +++ b/en/sandbox/webhooks.mdx @@ -0,0 +1,111 @@ +--- +title: 'Webhooks' +description: 'How the sandbox delivers webhooks and how to test dedupe, retries, and signature.' +mode: 'wide' +--- + +## How it works + +The sandbox uses the **same outbox engine** as production. That means: + +- Same payload structure +- Same headers (`X-NTXPay-Delivery`, `X-NTXPay-Signature`, etc.) +- Same exponential retry policy +- Same HMAC signature format + +The only difference is **speed**: sandbox webhooks fire ~1 second after the request (vs. minutes in production), and you can force artificial delays via the `delayed:` scenario. + +## Register URL + +```bash +curl -X POST https://sandbox.ntxpay.com/api/webhooks-config \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "url": "https://my-server.com/webhooks/ntxpay", + "events": ["cash_in", "cash_out"] + }' +``` + +### Response (201) + +```json +{ + "id": "wh_550e8400", + "url": "https://my-server.com/webhooks/ntxpay", + "events": ["cash_in", "cash_out"], + "secret": "whsec_a1b2c3d4...", + "createdAt": "2026-03-26T09:00:00.000Z" +} +``` + +Save the returned `secret` — it is used to verify the HMAC signature. **It is only displayed once.** + +## Available events + +| Event | Fired when | +|---|---| +| `cash_in` | Disposable CLABE receives a (simulated) transfer | +| `cash_out` | SPEI send resolves (confirmed or failed) | +| `refund_in` | Cash-in refund is processed | +| `refund_out` | Cash-out refund is processed | + +## Verify the signature + +Each webhook arrives with the `X-NTXPay-Signature` header in the `sha256=` format: + +```python +import hmac +import hashlib + +def verify(payload_bytes: bytes, signature_header: str, secret: str) -> bool: + expected = "sha256=" + hmac.new( + secret.encode(), + payload_bytes, + hashlib.sha256, + ).hexdigest() + return hmac.compare_digest(expected, signature_header) +``` + +## Test dedupe + +Each delivery has a unique `deliveryId` in the `X-NTXPay-Delivery` header and inside the payload. To test your dedupe: + +1. Make your handler return `500` on the first attempt. +2. NTX Pay will deliver the same message again (with the **same** `deliveryId`). +3. Confirm your system ignores the duplicate and responds `200` on the second attempt. + +## Retry policy + +| Attempt | Delay after previous | +|---|---| +| 1 | immediate | +| 2 | 30s | +| 3 | 2min | +| 4 | 10min | +| 5 | 1h | +| 6 | 6h | +| 7+ | given up | + +Your endpoint must respond `2xx` within **5 seconds** — any `5xx`, timeout, or connection error triggers a retry. + +## Test scenarios + +Force the webhook to come back as `FAILED` or with delay: + +```bash +curl -X POST https://sandbox.ntxpay.com/api/spei/cash-out \ + -H "Authorization: Bearer $TOKEN" \ + -H "X-Sandbox-Scenario: delayed:30s" \ + -H "Content-Type: application/json" \ + -d '{ ... }' +``` + +See [Scenarios](/en/sandbox/scenarios) for the full catalog. + +## Best practices + +1. **Respond 200 before processing** — queue the event in the background; five seconds is the ceiling. +2. **Use `deliveryId` for dedupe** — do not rely on `transaction.id` (retries arrive with the same `transaction.id` but a new `deliveryId` on manual redrive). +3. **Don't depend on order** — webhooks can arrive out of order after retries. +4. **Always verify the signature** — even in sandbox. diff --git a/es/sandbox/authentication.mdx b/es/sandbox/authentication.mdx new file mode 100644 index 0000000..b6d1274 --- /dev/null +++ b/es/sandbox/authentication.mdx @@ -0,0 +1,81 @@ +--- +title: 'Autenticación' +description: 'La autenticación en sandbox es estructuralmente idéntica a producción.' +mode: 'wide' +--- + +## Visión General + +La autenticación de sandbox usa las mismas dos capas que producción: + +1. **Certificado X.509 (mTLS)** — entregado por NTX Pay en el onboarding. +2. **OAuth 2.0 `client_credentials`** — `clientId` + `clientSecret` recibidos en el signup. + +En conjunto, devuelven un **JWT** (validez de 10 minutos) usado en los demás endpoints como `Authorization: Bearer ...`. + + + Las credenciales de sandbox son **distintas** de las de producción. Si usas credenciales de producción contra `https://sandbox.ntxpay.com`, recibirás `401`. El contrato HTTP es idéntico — lo que cambia es el par certificado + clientId/clientSecret. + + +## Obtener Token + +### POST /api/auth/token + +```bash +curl -X POST https://sandbox.ntxpay.com/api/auth/token \ + -H "X-SSL-Client-Cert: $ENCODED_CERT" \ + -H "Content-Type: application/json" \ + -d '{ + "clientId": "qr-93-550e8400", + "clientSecret": "a1b2c3d4e5f6g7h8" + }' +``` + +#### Response (201) + +```json +{ + "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "token_type": "Bearer", + "expires_in": 600, + "scope": "email profile" +} +``` + +## Usar el Token + +En una cuenta sandbox, cualquier llamada autenticada simula el pipeline completo sin mover dinero real: + +```bash +curl -X POST https://sandbox.ntxpay.com/api/spei/cash-out \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "amountCentavos": 15000, + "destinationClabe": "012180001234567890", + "beneficiaryName": "Maria Lopez", + "externalId": "test-001" + }' +``` + +La respuesta siempre es `201 Created` con `status: PENDING`. El resultado final (confirmación o falla) llega vía webhook tras ~1 segundo. Mira [Escenarios](/es/sandbox/scenarios) para forzar resultados específicos. + +## Renovación + +El token expira en **10 minutos (600s)**. No hay refresh token — genera uno nuevo vía `POST /api/auth/token` antes de que expire. + + + Bajo alta carga, genera un token por worker y renueva cada ~8 minutos para evitar `401` por expiración. + + +## Errores Comunes + +| Código | Causa | Solución | +|---|---|---| +| `400` | `X-SSL-Client-Cert` ausente | Configura NGINX/ALB para reenviar el certificado | +| `401` | `clientId`/`clientSecret` inválido | Verifica las credenciales; confirma que estás usando las de sandbox | +| `401` | Certificado expirado/revocado | Solicita renovación a NTX Pay | + +## Documentación detallada + +Para el paso a paso completo (codificación del certificado, ejemplos en múltiples lenguajes, etc.) mira [Autenticación](/es/guides/authentication) en la guía general — la única diferencia es la base URL `https://sandbox.ntxpay.com`. diff --git a/es/sandbox/cash-in.mdx b/es/sandbox/cash-in.mdx new file mode 100644 index 0000000..bc0fb98 --- /dev/null +++ b/es/sandbox/cash-in.mdx @@ -0,0 +1,88 @@ +--- +title: 'Cash-in (recepción SPEI)' +description: 'Cómo generar una CLABE desechable de cobro en sandbox.' +mode: 'wide' +--- + +## Qué hace + +`POST /api/spei/cash-in` genera una **CLABE desechable** vinculada a tu cuenta sandbox. Cualquier transferencia SPEI recibida en esa CLABE dispara un webhook `cash_in` a la URL configurada. + +En sandbox, la confirmación es **simulada** ~1 segundo después de crear la CLABE (en lugar de esperar una transferencia real). Esto permite probar todo el flujo de cash-in sin depender de un banco emisor real. + +## Ejemplo + +```bash +curl -X POST https://sandbox.ntxpay.com/api/spei/cash-in \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "amountCentavos": 50000, + "externalId": "order-001", + "customerName": "Juan Pérez" + }' +``` + +### Response (201) + +```json +{ + "id": 12345, + "externalId": "order-001", + "status": "PENDING", + "amountCentavos": 50000, + "clabe": "646180123456789012", + "expiresAt": "2026-03-26T10:30:00.000Z" +} +``` + +Usa la `clabe` devuelta para mostrarla al pagador final (cliente de tu empresa). En sandbox, esta CLABE es ficticia pero el campo `transaction.clabe` que llega en el webhook será **el mismo**. + +## Webhook esperado + +Tras ~1 segundo (escenario `success` default), recibes: + +```json +{ + "event": "cash_in", + "deliveryId": "...", + "transaction": { + "id": 12345, + "externalId": "order-001", + "status": "CONFIRMED", + "amountCentavos": 50000, + "clabe": "646180123456789012", + "confirmedAt": "2026-03-26T10:00:01.000Z", + "counterpart": { + "name": "Pagador Simulado", + "taxId": "PAGS850101ABC", + "bank": { + "code": "012", + "name": "BBVA México" + } + } + } +} +``` + +## Escenarios de prueba + +| Escenario | Webhook | +|---|---| +| Default | `CONFIRMED` | +| `error:invalid-clabe` | `FAILED` | +| `error:duplicate-external-id` | `FAILED` | +| `delayed:5s` | `CONFIRMED` tras 5s | + +Mira [Escenarios](/es/sandbox/scenarios) para la lista completa. + +## Próximos pasos + + + + Cómo enviar SPEI en sandbox. + + + Entendiendo la entrega de webhooks en sandbox. + + diff --git a/es/sandbox/cash-out.mdx b/es/sandbox/cash-out.mdx new file mode 100644 index 0000000..2f5b3f7 --- /dev/null +++ b/es/sandbox/cash-out.mdx @@ -0,0 +1,102 @@ +--- +title: 'Cash-out (envío SPEI)' +description: 'Cómo enviar SPEI a una CLABE de destino en sandbox.' +mode: 'wide' +--- + +## Qué hace + +`POST /api/spei/cash-out` solicita el envío de SPEI a una CLABE de destino. En sandbox, el pipeline contable completo se ejercita — el saldo se debita, la tarifa se cobra, se genera registro en el extracto — pero la llamada a Banxico es simulada. + +La respuesta HTTP siempre es `201 Created` con `status: PENDING`. El resultado final llega vía webhook `cash_out` ~1 segundo después (escenario `success`) o según el escenario forzado. + +## Prerrequisito + +Tu cuenta sandbox necesita saldo. Realiza al menos un [cash-in](/es/sandbox/cash-in) antes — el saldo simulado se debita igual que en producción. + +## Ejemplo + +```bash +curl -X POST https://sandbox.ntxpay.com/api/spei/cash-out \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "amountCentavos": 15000, + "destinationClabe": "012180001234567890", + "beneficiaryName": "Maria Lopez", + "beneficiaryTaxId": "LOPM850101ABC", + "externalId": "payout-001", + "description": "Pago a proveedor" + }' +``` + +### Response (201) + +```json +{ + "id": 12346, + "externalId": "payout-001", + "status": "PENDING", + "amountCentavos": 15000, + "clabe": "012180001234567890" +} +``` + +## Webhook esperado + +Tras ~1 segundo (escenario `success`): + +```json +{ + "event": "cash_out", + "deliveryId": "...", + "transaction": { + "id": 12346, + "externalId": "payout-001", + "status": "CONFIRMED", + "amountCentavos": 15000, + "clabe": "012180001234567890", + "referenceNumerical": "9876543", + "confirmedAt": "2026-03-26T10:00:01.000Z" + }, + "errorCode": null, + "errorMessage": null +} +``` + +## Escenarios de error útiles + +Fuerza comportamientos específicos vía el header `X-Sandbox-Scenario`: + +```bash +curl -X POST https://sandbox.ntxpay.com/api/spei/cash-out \ + -H "Authorization: Bearer $TOKEN" \ + -H "X-Sandbox-Scenario: error:insufficient-funds" \ + -H "Content-Type: application/json" \ + -d '{ ... }' +``` + +| Escenario | Cuándo usar | +|---|---| +| `error:insufficient-funds` | Validar UX cuando tu cliente intenta pagar sin saldo | +| `error:invalid-clabe` | Validar parsing/validación de CLABE en tu frontend | +| `error:account-not-found` | Validar error post-red (CLABE existe en tu base, pero no en la red SPEI) | +| `error:bank-rejected` | Validar fallback genérico | +| `delayed:30s` | Validar UX de "transferencia en progreso" | + +Mira [Escenarios](/es/sandbox/scenarios) para la lista completa. + +## Validaciones síncronas + +Incluso en sandbox, algunas validaciones ocurren **antes** del retorno `201`: + +| Error síncrono | HTTP | Cuándo | +|---|---|---| +| `400 INVALID_AMOUNT` | `400` | `amountCentavos <= 0` | +| `400 INVALID_CLABE_FORMAT` | `400` | CLABE con formato inválido (no-numérica, longitud incorrecta) | +| `400 DUPLICATE_EXTERNAL_ID` | `400` | `externalId` ya usado en otra transacción de esa cuenta | +| `400 INSUFFICIENT_FUNDS` | `400` | Saldo real por debajo de `amountCentavos + tarifa` (sin usar escenario) | + + + Los escenarios con prefijo `error:` afectan al **webhook** — la respuesta síncrona siempre es `201 PENDING`. Las validaciones estructurales como formato/duplicidad fallan de forma síncrona. + diff --git a/es/sandbox/introduction.mdx b/es/sandbox/introduction.mdx new file mode 100644 index 0000000..bff42f2 --- /dev/null +++ b/es/sandbox/introduction.mdx @@ -0,0 +1,68 @@ +--- +title: 'Sandbox NTX Pay' +description: 'Ambiente de pruebas con alta fidelidad al pipeline de producción de NTX Pay México.' +mode: 'wide' +--- + +## Qué es + +El sandbox de NTX Pay permite que tu integración ejercite **cash-in**, **cash-out**, **refund** y **webhooks** sin mover dinero real. A diferencia de mocks simples, el pipeline contable completo (saldo TigerBeetle, validación de límites, cobro de tarifas, generación de extractos, entrega de webhooks vía outbox) se ejercita intacto. Solo el provider externo (SPEI/Banxico) es simulado. + + + Toda integración con NTX Pay empieza por el sandbox. Los endpoints, payloads y webhooks descritos en esta documentación son los definitivos — cuando producción se habilite para tu empresa, el mismo código funcionará simplemente cambiando las credenciales. + + +## Cómo activar + +Tus credenciales de API son **estructuralmente las mismas** que usarías en producción. La diferencia vive en la cuenta: las cuentas con `mainProvider: "sandbox"` enrutan toda llamada SPEI internamente al simulador NTX. Para crear una cuenta sandbox, contacta a tu Account Manager o escribe a `contact@ntxpay.com` — el onboarding es instantáneo y el KYC es auto-aprobado. + +## Base URL + +| Ambiente | URL | +|---|---| +| Sandbox | `https://sandbox.ntxpay.com` | + +Todas las rutas documentadas (`/api/auth/token`, `/api/spei/cash-in`, `/api/spei/cash-out`, `/api/oxxo/cash-in`, `/api/transactions`, `/api/webhooks-config`) están disponibles exactamente en este host. + +## Escenarios de prueba + +Controlas el comportamiento de cada llamada vía el header HTTP `X-Sandbox-Scenario`. Sin el header, el sandbox devuelve **éxito** por defecto. Mira [Escenarios](/es/sandbox/scenarios) para la lista completa de escenarios de error, éxito y atraso soportados. + +## Webhooks + +Registra tu `webhookUrl` en la cuenta sandbox exactamente como lo harías en producción — vía `POST /api/webhooks-config`. Los eventos son entregados por el mismo motor de outbox que usamos en prod, con las mismas firmas, headers (`X-NTXPay-Delivery`) y política de retry. + +## Diferencias vs Producción + +| Aspecto | Sandbox | Producción | +|---|---|---| +| Base URL | `https://sandbox.ntxpay.com` | `https://api.ntxpay.com` | +| Provider | `sandbox` (simulado) | Banco real (Banxico/SPEI) | +| Saldo | Simulado | Fondos reales | +| Confirmación SPEI cash-in | Inmediata (~1s) | Real (segundos a minutos) | +| `X-Sandbox-Scenario` | Soportado | Rechazado con `400` | +| Costo | Gratis | Según contrato | + +## Próximos pasos + + + + Cómo obtener el JWT en sandbox usando tus credenciales. + + + Lista completa de escenarios disponibles vía `X-Sandbox-Scenario`. + + + Recibir vía SPEI en sandbox. + + + Enviar vía SPEI en sandbox. + + + Cómo el sandbox entrega webhooks y cómo probar dedupe. + + + +## Soporte + +`contact@ntxpay.com` diff --git a/es/sandbox/scenarios.mdx b/es/sandbox/scenarios.mdx new file mode 100644 index 0000000..d16c7fc --- /dev/null +++ b/es/sandbox/scenarios.mdx @@ -0,0 +1,141 @@ +--- +title: 'Escenarios de prueba' +description: 'Fuerza comportamientos específicos vía el header X-Sandbox-Scenario.' +mode: 'wide' +--- + +## Cómo usar + +Agrega el header `X-Sandbox-Scenario: ` a cualquier llamada de cash-in, cash-out u OXXO. Sin el header, el sandbox usa el escenario `success` por defecto. + +```bash +curl -X POST https://sandbox.ntxpay.com/api/spei/cash-out \ + -H "Authorization: Bearer $TOKEN" \ + -H "X-Sandbox-Scenario: error:insufficient-funds" \ + -H "Content-Type: application/json" \ + -d '{ + "amountCentavos": 15000, + "destinationClabe": "012180001234567890", + "beneficiaryName": "Maria Lopez", + "externalId": "test-error-001" + }' +``` + + + El header solo controla el **webhook asíncrono**. La respuesta HTTP siempre es `201 Created` con `status: PENDING`, sin importar el escenario. El resultado final (`CONFIRMED`, `FAILED` o `EXPIRED`) llega en el webhook ~1 segundo después. + + +## Escenarios disponibles + +### Escenarios de éxito + +| Header Value | Comportamiento síncrono | Webhook | +|---|---|---| +| `success` (default) | `201 PENDING` | `CONFIRMED` en ~1s | +| _(sin header)_ | `201 PENDING` | `CONFIRMED` en ~1s | + +### Escenarios de error + +| Header Value | Comportamiento síncrono | Webhook | +|---|---|---| +| `error:insufficient-funds` | `201 PENDING` | `FAILED` con `errorCode: INSUFFICIENT_FUNDS` | +| `error:invalid-clabe` | `201 PENDING` | `FAILED` con `errorCode: INVALID_CLABE` | +| `error:account-not-found` | `201 PENDING` | `FAILED` con `errorCode: ACCOUNT_NOT_FOUND` | +| `error:account-blocked` | `201 PENDING` | `FAILED` con `errorCode: ACCOUNT_BLOCKED` | +| `error:duplicate-external-id` | `201 PENDING` | `FAILED` con `errorCode: DUPLICATE_EXTERNAL_ID` | +| `error:bank-rejected` | `201 PENDING` | `FAILED` con `errorCode: BANK_REJECTED` | +| `error:oxxo-expired` | `201 PENDING` | `EXPIRED` (solo OXXO) | + +### Escenarios de atraso + +| Header Value | Comportamiento síncrono | Webhook | +|---|---|---| +| `delayed:5s` | `201 PENDING` | `CONFIRMED` tras +5s | +| `delayed:30s` | `201 PENDING` | `CONFIRMED` tras +30s | +| `delayed:60s` | `201 PENDING` | `CONFIRMED` tras +60s | + + + El delay máximo permitido es de **120 segundos** — valores mayores se truncan automáticamente. + + +## Ejemplo: webhook de éxito + +```json +{ + "event": "cash_out", + "deliveryId": "8e2c5b6f-3a12-4b9c-9a18-77a2b3c4d5e6", + "createdAt": "2026-03-26T10:00:00.000Z", + "transaction": { + "id": 12345, + "externalId": "test-success-001", + "paymentMethod": "SPEI", + "direction": "out", + "type": "cash_out", + "status": "CONFIRMED", + "provider": "sandbox", + "amountCentavos": 15000, + "clabe": "012180001234567890", + "referenceNumerical": "9876543", + "createdAt": "2026-03-26T09:59:59.000Z", + "confirmedAt": "2026-03-26T10:00:00.000Z", + "counterpart": { + "name": "Maria Lopez", + "taxId": null, + "bank": {} + } + }, + "errorCode": null, + "errorMessage": null, + "metadata": {} +} +``` + +## Ejemplo: webhook de falla + +```json +{ + "event": "cash_out", + "deliveryId": "1a3f9e8d-2c47-4b9c-aa18-77a2b3c4d5e6", + "createdAt": "2026-03-26T10:01:00.000Z", + "transaction": { + "id": 12346, + "externalId": "test-error-001", + "paymentMethod": "SPEI", + "direction": "out", + "type": "cash_out", + "status": "FAILED", + "provider": "sandbox", + "amountCentavos": 15000, + "clabe": "012180001234567890", + "referenceNumerical": null, + "confirmedAt": null + }, + "errorCode": "INSUFFICIENT_FUNDS", + "errorMessage": "Cuenta sin saldo suficiente", + "metadata": {} +} +``` + +Notas: + +- En `status: FAILED`, `referenceNumerical` y `confirmedAt` son `null` — la red SPEI nunca confirmó la transacción. +- `errorCode` y `errorMessage` describen el motivo de la falla. + +## Restricciones + +- El header `X-Sandbox-Scenario` funciona **exclusivamente** en cuentas sandbox. +- Las cuentas de producción que envíen el header reciben: + +```json +{ + "statusCode": 400, + "message": "X-Sandbox-Scenario header is only supported in sandbox mode." +} +``` + +## Buenas prácticas + +1. **Prueba todos los escenarios** antes de ir a producción — implementa el manejo de `CONFIRMED`, `PENDING`, `FAILED` y `EXPIRED`. +2. **Valida los campos de error** — usa `errorCode` para decisiones automáticas; reserva `errorMessage` para logs/usuarios. +3. **Prueba con delay** — verifica que tu sistema maneja bien la entrega lenta del webhook. +4. **Idempotencia** — usa `transaction.id` como clave de idempotencia; el mismo webhook puede ser re-entregado. diff --git a/es/sandbox/webhooks.mdx b/es/sandbox/webhooks.mdx new file mode 100644 index 0000000..48d68bd --- /dev/null +++ b/es/sandbox/webhooks.mdx @@ -0,0 +1,111 @@ +--- +title: 'Webhooks' +description: 'Cómo el sandbox entrega webhooks y cómo probar dedupe, retries y firma.' +mode: 'wide' +--- + +## Cómo funciona + +El sandbox usa el **mismo motor de outbox** que producción. Eso significa: + +- Misma estructura de payload +- Mismos headers (`X-NTXPay-Delivery`, `X-NTXPay-Signature`, etc.) +- Misma política de retry exponencial +- Mismo formato de firma HMAC + +La única diferencia es la **velocidad**: los webhooks de sandbox son disparados ~1 segundo después de la request (vs. minutos en producción), y puedes forzar atrasos artificiales vía el escenario `delayed:`. + +## Registrar URL + +```bash +curl -X POST https://sandbox.ntxpay.com/api/webhooks-config \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "url": "https://mi-servidor.com/webhooks/ntxpay", + "events": ["cash_in", "cash_out"] + }' +``` + +### Response (201) + +```json +{ + "id": "wh_550e8400", + "url": "https://mi-servidor.com/webhooks/ntxpay", + "events": ["cash_in", "cash_out"], + "secret": "whsec_a1b2c3d4...", + "createdAt": "2026-03-26T09:00:00.000Z" +} +``` + +Guarda el `secret` devuelto — se usa para verificar la firma HMAC. **Solo se muestra una vez.** + +## Eventos disponibles + +| Evento | Disparado cuando | +|---|---| +| `cash_in` | CLABE desechable recibe una transferencia (simulada) | +| `cash_out` | Envío SPEI se resuelve (confirmado o falló) | +| `refund_in` | Refund de cash-in se procesa | +| `refund_out` | Refund de cash-out se procesa | + +## Verificar la firma + +Cada webhook llega con el header `X-NTXPay-Signature` en el formato `sha256=`: + +```python +import hmac +import hashlib + +def verify(payload_bytes: bytes, signature_header: str, secret: str) -> bool: + expected = "sha256=" + hmac.new( + secret.encode(), + payload_bytes, + hashlib.sha256, + ).hexdigest() + return hmac.compare_digest(expected, signature_header) +``` + +## Probar dedupe + +Cada entrega tiene un `deliveryId` único en el header `X-NTXPay-Delivery` y dentro del payload. Para probar tu dedupe: + +1. Configura tu handler para retornar `500` en el primer intento. +2. NTX Pay entregará el mismo mensaje nuevamente (con el **mismo** `deliveryId`). +3. Confirma que tu sistema ignora la duplicada y responde `200` en el segundo intento. + +## Política de retry + +| Intento | Atraso tras el anterior | +|---|---| +| 1 | inmediato | +| 2 | 30s | +| 3 | 2min | +| 4 | 10min | +| 5 | 1h | +| 6 | 6h | +| 7+ | abandonado | + +Tu endpoint necesita responder `2xx` en hasta **5 segundos** — cualquier `5xx`, timeout o error de conexión dispara retry. + +## Escenarios de prueba + +Fuerza que el webhook salga como `FAILED` o con delay: + +```bash +curl -X POST https://sandbox.ntxpay.com/api/spei/cash-out \ + -H "Authorization: Bearer $TOKEN" \ + -H "X-Sandbox-Scenario: delayed:30s" \ + -H "Content-Type: application/json" \ + -d '{ ... }' +``` + +Mira [Escenarios](/es/sandbox/scenarios) para el catálogo completo. + +## Buenas prácticas + +1. **Responde 200 antes de procesar** — encola el evento en background; cinco segundos es el tope. +2. **Usa `deliveryId` para dedupe** — no confíes en `transaction.id` (los retries llegan con el mismo `transaction.id` pero `deliveryId` nuevo en caso de redrive manual). +3. **No dependas del orden** — los webhooks pueden llegar fuera de orden tras retries. +4. **Valida siempre la firma** — incluso en sandbox. diff --git a/pt-br/sandbox/authentication.mdx b/pt-br/sandbox/authentication.mdx new file mode 100644 index 0000000..b0b00a6 --- /dev/null +++ b/pt-br/sandbox/authentication.mdx @@ -0,0 +1,81 @@ +--- +title: 'Autenticação' +description: 'Autenticação em sandbox é estruturalmente idêntica à produção.' +mode: 'wide' +--- + +## Visão Geral + +A autenticação em sandbox usa as mesmas duas camadas que produção: + +1. **Certificado X.509 (mTLS)** — entregue pelo NTX Pay no onboarding. +2. **OAuth 2.0 `client_credentials`** — `clientId` + `clientSecret` recebidos no signup. + +Em conjunto, retornam um **JWT** (validade 10 minutos) usado nos demais endpoints como `Authorization: Bearer ...`. + + + As credenciais de sandbox são **distintas** das de produção. Se você usar credenciais de produção contra `https://sandbox.ntxpay.com`, receberá `401`. O contrato HTTP é idêntico — o que muda é o par certificado + clientId/clientSecret. + + +## Obter Token + +### POST /api/auth/token + +```bash +curl -X POST https://sandbox.ntxpay.com/api/auth/token \ + -H "X-SSL-Client-Cert: $ENCODED_CERT" \ + -H "Content-Type: application/json" \ + -d '{ + "clientId": "qr-93-550e8400", + "clientSecret": "a1b2c3d4e5f6g7h8" + }' +``` + +#### Response (201) + +```json +{ + "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "token_type": "Bearer", + "expires_in": 600, + "scope": "email profile" +} +``` + +## Usar o Token + +Em uma conta sandbox, qualquer chamada autenticada simula um pipeline completo sem mover dinheiro real: + +```bash +curl -X POST https://sandbox.ntxpay.com/api/spei/cash-out \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "amountCentavos": 15000, + "destinationClabe": "012180001234567890", + "beneficiaryName": "Maria Lopez", + "externalId": "test-001" + }' +``` + +A resposta é sempre `201 Created` com `status: PENDING`. O resultado final (confirmação ou falha) chega via webhook após ~1 segundo. Veja [Cenários](/pt-br/sandbox/scenarios) para forçar resultados específicos. + +## Renovação + +O token expira em **10 minutos (600s)**. Não há refresh token — gere um novo via `POST /api/auth/token` antes de expirar. + + + Em alta carga, gere um token por worker e renove a cada ~8 minutos para evitar `401` por expiração. + + +## Erros Comuns + +| Código | Causa | Solução | +|---|---|---| +| `400` | `X-SSL-Client-Cert` ausente | Configure NGINX/ALB para repassar o certificado | +| `401` | `clientId`/`clientSecret` inválido | Reconfira credenciais; certifique-se de estar usando as de sandbox | +| `401` | Certificado expirado/revogado | Solicite renovação ao NTX Pay | + +## Documentação detalhada + +Para o passo a passo completo (encoding do certificado, exemplos em múltiplas linguagens, etc.) veja [Autenticação](/pt-br/guides/authentication) no guia geral — a única diferença é a base URL `https://sandbox.ntxpay.com`. diff --git a/pt-br/sandbox/cash-in.mdx b/pt-br/sandbox/cash-in.mdx new file mode 100644 index 0000000..2229f82 --- /dev/null +++ b/pt-br/sandbox/cash-in.mdx @@ -0,0 +1,88 @@ +--- +title: 'Cash-in (recebimento SPEI)' +description: 'Como gerar CLABE descartável de cobrança no sandbox.' +mode: 'wide' +--- + +## O que faz + +`POST /api/spei/cash-in` gera uma **CLABE descartável** vinculada à sua conta sandbox. Qualquer transferência SPEI recebida nessa CLABE dispara um webhook `cash_in` para a URL configurada. + +No sandbox, a confirmação é **simulada** ~1 segundo após a criação da CLABE (em vez de aguardar uma transferência real). Isso permite testar todo o fluxo de cash-in sem depender de um banco emissor. + +## Exemplo + +```bash +curl -X POST https://sandbox.ntxpay.com/api/spei/cash-in \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "amountCentavos": 50000, + "externalId": "order-001", + "customerName": "Juan Pérez" + }' +``` + +### Response (201) + +```json +{ + "id": 12345, + "externalId": "order-001", + "status": "PENDING", + "amountCentavos": 50000, + "clabe": "646180123456789012", + "expiresAt": "2026-03-26T10:30:00.000Z" +} +``` + +Use o `clabe` retornado para exibir ao pagador final (cliente da sua empresa). No sandbox, esta CLABE é fictícia mas o objeto `transaction.clabe` que chega no webhook será **o mesmo**. + +## Webhook esperado + +Após ~1 segundo (cenário `success` padrão), você recebe: + +```json +{ + "event": "cash_in", + "deliveryId": "...", + "transaction": { + "id": 12345, + "externalId": "order-001", + "status": "CONFIRMED", + "amountCentavos": 50000, + "clabe": "646180123456789012", + "confirmedAt": "2026-03-26T10:00:01.000Z", + "counterpart": { + "name": "Pagador Simulado", + "taxId": "PAGS850101ABC", + "bank": { + "code": "012", + "name": "BBVA México" + } + } + } +} +``` + +## Cenários de teste + +| Cenário | Webhook | +|---|---| +| Default | `CONFIRMED` | +| `error:invalid-clabe` | `FAILED` | +| `error:duplicate-external-id` | `FAILED` | +| `delayed:5s` | `CONFIRMED` após 5s | + +Veja [Cenários](/pt-br/sandbox/scenarios) para a lista completa. + +## Próximos passos + + + + Como enviar SPEI no sandbox. + + + Entendendo a entrega de webhooks no sandbox. + + diff --git a/pt-br/sandbox/cash-out.mdx b/pt-br/sandbox/cash-out.mdx new file mode 100644 index 0000000..6ed370e --- /dev/null +++ b/pt-br/sandbox/cash-out.mdx @@ -0,0 +1,102 @@ +--- +title: 'Cash-out (envio SPEI)' +description: 'Como enviar SPEI a uma CLABE de destino no sandbox.' +mode: 'wide' +--- + +## O que faz + +`POST /api/spei/cash-out` solicita o envio de SPEI a uma CLABE de destino. No sandbox, o pipeline contábil completo é exercitado — saldo é debitado, tarifa é cobrada, registro no extrato é gerado — mas a chamada ao Banxico é simulada. + +A resposta HTTP é sempre `201 Created` com `status: PENDING`. O resultado final chega via webhook `cash_out` ~1 segundo depois (cenário `success`) ou conforme o cenário forçado. + +## Pré-requisito + +Sua conta sandbox precisa ter saldo. Faça pelo menos um [cash-in](/pt-br/sandbox/cash-in) antes — o saldo simulado é debitado igual produção. + +## Exemplo + +```bash +curl -X POST https://sandbox.ntxpay.com/api/spei/cash-out \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "amountCentavos": 15000, + "destinationClabe": "012180001234567890", + "beneficiaryName": "Maria Lopez", + "beneficiaryTaxId": "LOPM850101ABC", + "externalId": "payout-001", + "description": "Pagamento de fornecedor" + }' +``` + +### Response (201) + +```json +{ + "id": 12346, + "externalId": "payout-001", + "status": "PENDING", + "amountCentavos": 15000, + "clabe": "012180001234567890" +} +``` + +## Webhook esperado + +Após ~1 segundo (cenário `success`): + +```json +{ + "event": "cash_out", + "deliveryId": "...", + "transaction": { + "id": 12346, + "externalId": "payout-001", + "status": "CONFIRMED", + "amountCentavos": 15000, + "clabe": "012180001234567890", + "referenceNumerical": "9876543", + "confirmedAt": "2026-03-26T10:00:01.000Z" + }, + "errorCode": null, + "errorMessage": null +} +``` + +## Cenários de erro úteis + +Force comportamentos específicos via header `X-Sandbox-Scenario`: + +```bash +curl -X POST https://sandbox.ntxpay.com/api/spei/cash-out \ + -H "Authorization: Bearer $TOKEN" \ + -H "X-Sandbox-Scenario: error:insufficient-funds" \ + -H "Content-Type: application/json" \ + -d '{ ... }' +``` + +| Cenário | Quando usar | +|---|---| +| `error:insufficient-funds` | Validar UX quando seu cliente tenta pagar sem saldo | +| `error:invalid-clabe` | Validar parsing/validação de CLABE no seu front | +| `error:account-not-found` | Validar erro pós-rede (CLABE existe na sua base, mas não na rede SPEI) | +| `error:bank-rejected` | Validar fallback genérico | +| `delayed:30s` | Validar UX de "transferência em andamento" | + +Veja [Cenários](/pt-br/sandbox/scenarios) para a lista completa. + +## Validações síncronas + +Mesmo no sandbox, algumas validações ocorrem **antes** do retorno `201`: + +| Erro síncrono | HTTP | Quando | +|---|---|---| +| `400 INVALID_AMOUNT` | `400` | `amountCentavos <= 0` | +| `400 INVALID_CLABE_FORMAT` | `400` | CLABE com formato inválido (não-numérica, comprimento errado) | +| `400 DUPLICATE_EXTERNAL_ID` | `400` | `externalId` já usado em outra transação dessa conta | +| `400 INSUFFICIENT_FUNDS` | `400` | Saldo real abaixo de `amountCentavos + tarifa` (sem usar cenário) | + + + Cenários com prefixo `error:` afetam o **webhook** — a resposta síncrona é sempre `201 PENDING`. Já validações estruturais como formato/duplicidade quebram síncrono. + diff --git a/pt-br/sandbox/introduction.mdx b/pt-br/sandbox/introduction.mdx new file mode 100644 index 0000000..9a0561c --- /dev/null +++ b/pt-br/sandbox/introduction.mdx @@ -0,0 +1,68 @@ +--- +title: 'Sandbox NTX Pay' +description: 'Ambiente de teste com alta fidelidade ao pipeline de produção do NTX Pay México.' +mode: 'wide' +--- + +## O que é + +O sandbox NTX Pay permite que sua integração exercite **cash-in**, **cash-out**, **refund** e **webhooks** sem mover dinheiro real. Diferente de mocks simplistas, o pipeline contábil completo (saldo TigerBeetle, validação de limites, cobrança de tarifas, geração de extratos, entrega de webhooks via outbox) é exercitado intacto. Apenas o provider externo (SPEI/Banxico) é simulado. + + + Toda integração com a NTX Pay começa pelo sandbox. Os endpoints, payloads e webhooks descritos nesta documentação são os definitivos — quando produção for liberada para a sua empresa, o mesmo código funcionará apenas trocando as credenciais. + + +## Como ativar + +Suas credenciais de API são as **mesmas estruturalmente** que você usaria em produção. A diferença vive na conta: contas com `mainProvider: "sandbox"` roteiam todas as chamadas SPEI internamente para o simulador NTX. Para criar uma conta sandbox, peça ao seu Account Manager ou escreva para `contact@ntxpay.com` — o onboarding é instantâneo e o KYC é auto-aprovado. + +## Base URL + +| Ambiente | URL | +|---|---| +| Sandbox | `https://sandbox.ntxpay.com` | + +Todas as rotas documentadas (`/api/auth/token`, `/api/spei/cash-in`, `/api/spei/cash-out`, `/api/oxxo/cash-in`, `/api/transactions`, `/api/webhooks-config`) estão disponíveis exatamente neste host. + +## Cenários de teste + +Você controla o comportamento de cada chamada via header HTTP `X-Sandbox-Scenario`. Sem o header, o sandbox retorna **sucesso** por padrão. Veja [Cenários](/pt-br/sandbox/scenarios) para a lista completa de cenários de erro, sucesso e atraso suportados. + +## Webhooks + +Registre seu `webhookUrl` na conta sandbox exatamente como faria em produção — via `POST /api/webhooks-config`. Os eventos são entregues pelo mesmo motor de outbox que usamos em prod, com as mesmas assinaturas, headers (`X-NTXPay-Delivery`) e política de retry. + +## Diferenças vs Produção + +| Aspecto | Sandbox | Produção | +|---|---|---| +| Base URL | `https://sandbox.ntxpay.com` | `https://api.ntxpay.com` | +| Provider | `sandbox` (simulado) | Banco real (Banxico/SPEI) | +| Saldo | Simulado | Fundos reais | +| Confirmação SPEI cash-in | Imediata (~1s) | Real (segundos a minutos) | +| `X-Sandbox-Scenario` | Suportado | Rejeitado com `400` | +| Custo | Gratuito | Conforme contrato | + +## Próximos passos + + + + Como obter o JWT no sandbox usando suas credenciais. + + + Lista completa de cenários disponíveis via `X-Sandbox-Scenario`. + + + Receber via SPEI no sandbox. + + + Enviar via SPEI no sandbox. + + + Como o sandbox entrega webhooks e como testar dedupe. + + + +## Suporte + +`suporte@ntxpay.com` diff --git a/pt-br/sandbox/scenarios.mdx b/pt-br/sandbox/scenarios.mdx new file mode 100644 index 0000000..906761a --- /dev/null +++ b/pt-br/sandbox/scenarios.mdx @@ -0,0 +1,141 @@ +--- +title: 'Cenários de teste' +description: 'Force comportamentos específicos via header X-Sandbox-Scenario.' +mode: 'wide' +--- + +## Como usar + +Adicione o header `X-Sandbox-Scenario: ` a qualquer chamada de cash-in, cash-out ou OXXO. Sem o header, o sandbox usa o cenário `success` por padrão. + +```bash +curl -X POST https://sandbox.ntxpay.com/api/spei/cash-out \ + -H "Authorization: Bearer $TOKEN" \ + -H "X-Sandbox-Scenario: error:insufficient-funds" \ + -H "Content-Type: application/json" \ + -d '{ + "amountCentavos": 15000, + "destinationClabe": "012180001234567890", + "beneficiaryName": "Maria Lopez", + "externalId": "test-error-001" + }' +``` + + + O header controla **apenas o webhook assíncrono**. A resposta HTTP é sempre `201 Created` com `status: PENDING`, independentemente do cenário. O resultado final (`CONFIRMED`, `FAILED` ou `EXPIRED`) chega no webhook ~1 segundo depois. + + +## Cenários disponíveis + +### Cenários de sucesso + +| Header Value | Comportamento síncrono | Webhook | +|---|---|---| +| `success` (default) | `201 PENDING` | `CONFIRMED` em ~1s | +| _(sem header)_ | `201 PENDING` | `CONFIRMED` em ~1s | + +### Cenários de erro + +| Header Value | Comportamento síncrono | Webhook | +|---|---|---| +| `error:insufficient-funds` | `201 PENDING` | `FAILED` com `errorCode: INSUFFICIENT_FUNDS` | +| `error:invalid-clabe` | `201 PENDING` | `FAILED` com `errorCode: INVALID_CLABE` | +| `error:account-not-found` | `201 PENDING` | `FAILED` com `errorCode: ACCOUNT_NOT_FOUND` | +| `error:account-blocked` | `201 PENDING` | `FAILED` com `errorCode: ACCOUNT_BLOCKED` | +| `error:duplicate-external-id` | `201 PENDING` | `FAILED` com `errorCode: DUPLICATE_EXTERNAL_ID` | +| `error:bank-rejected` | `201 PENDING` | `FAILED` com `errorCode: BANK_REJECTED` | +| `error:oxxo-expired` | `201 PENDING` | `EXPIRED` (apenas OXXO) | + +### Cenários de atraso + +| Header Value | Comportamento síncrono | Webhook | +|---|---|---| +| `delayed:5s` | `201 PENDING` | `CONFIRMED` após +5s | +| `delayed:30s` | `201 PENDING` | `CONFIRMED` após +30s | +| `delayed:60s` | `201 PENDING` | `CONFIRMED` após +60s | + + + O delay máximo permitido é de **120 segundos** — valores acima são truncados automaticamente. + + +## Exemplo: webhook de sucesso + +```json +{ + "event": "cash_out", + "deliveryId": "8e2c5b6f-3a12-4b9c-9a18-77a2b3c4d5e6", + "createdAt": "2026-03-26T10:00:00.000Z", + "transaction": { + "id": 12345, + "externalId": "test-success-001", + "paymentMethod": "SPEI", + "direction": "out", + "type": "cash_out", + "status": "CONFIRMED", + "provider": "sandbox", + "amountCentavos": 15000, + "clabe": "012180001234567890", + "referenceNumerical": "9876543", + "createdAt": "2026-03-26T09:59:59.000Z", + "confirmedAt": "2026-03-26T10:00:00.000Z", + "counterpart": { + "name": "Maria Lopez", + "taxId": null, + "bank": {} + } + }, + "errorCode": null, + "errorMessage": null, + "metadata": {} +} +``` + +## Exemplo: webhook de falha + +```json +{ + "event": "cash_out", + "deliveryId": "1a3f9e8d-2c47-4b9c-aa18-77a2b3c4d5e6", + "createdAt": "2026-03-26T10:01:00.000Z", + "transaction": { + "id": 12346, + "externalId": "test-error-001", + "paymentMethod": "SPEI", + "direction": "out", + "type": "cash_out", + "status": "FAILED", + "provider": "sandbox", + "amountCentavos": 15000, + "clabe": "012180001234567890", + "referenceNumerical": null, + "confirmedAt": null + }, + "errorCode": "INSUFFICIENT_FUNDS", + "errorMessage": "Conta sem saldo suficiente", + "metadata": {} +} +``` + +Notas: + +- Em `status: FAILED`, `referenceNumerical` e `confirmedAt` são `null` — a rede SPEI nunca confirmou a transação. +- `errorCode` e `errorMessage` descrevem o motivo da falha. + +## Restrições + +- O header `X-Sandbox-Scenario` funciona **exclusivamente** em contas sandbox. +- Contas de produção que enviem o header recebem: + +```json +{ + "statusCode": 400, + "message": "X-Sandbox-Scenario header is only supported in sandbox mode." +} +``` + +## Boas práticas + +1. **Teste todos os cenários** antes de ir ao ar — implemente o tratamento de `CONFIRMED`, `PENDING`, `FAILED` e `EXPIRED`. +2. **Valide os campos de erro** — use `errorCode` para decisões automáticas; reserve `errorMessage` para logs/usuários. +3. **Teste com delay** — verifique que seu sistema lida bem com entrega lenta do webhook. +4. **Idempotência** — use `transaction.id` como chave de idempotência; o mesmo webhook pode ser reenviado. diff --git a/pt-br/sandbox/webhooks.mdx b/pt-br/sandbox/webhooks.mdx new file mode 100644 index 0000000..c4b67cc --- /dev/null +++ b/pt-br/sandbox/webhooks.mdx @@ -0,0 +1,111 @@ +--- +title: 'Webhooks' +description: 'Como o sandbox entrega webhooks e como testar dedupe, retry e assinatura.' +mode: 'wide' +--- + +## Como funciona + +O sandbox usa o **mesmo motor de outbox** que produção. Isso significa: + +- Mesma estrutura de payload +- Mesmos headers (`X-NTXPay-Delivery`, `X-NTXPay-Signature`, etc.) +- Mesma política de retry exponencial +- Mesmo formato de assinatura HMAC + +A única diferença é a **velocidade**: webhooks de sandbox são disparados ~1 segundo após a request (vs. minutos em produção), e você pode forçar atrasos artificiais via cenário `delayed:`. + +## Registrar URL + +```bash +curl -X POST https://sandbox.ntxpay.com/api/webhooks-config \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "url": "https://meu-servidor.com/webhooks/ntxpay", + "events": ["cash_in", "cash_out"] + }' +``` + +### Response (201) + +```json +{ + "id": "wh_550e8400", + "url": "https://meu-servidor.com/webhooks/ntxpay", + "events": ["cash_in", "cash_out"], + "secret": "whsec_a1b2c3d4...", + "createdAt": "2026-03-26T09:00:00.000Z" +} +``` + +Guarde o `secret` retornado — ele é usado para verificar a assinatura HMAC. **Ele só é exibido uma vez.** + +## Eventos disponíveis + +| Evento | Disparado quando | +|---|---| +| `cash_in` | CLABE descartável recebe uma transferência (simulada) | +| `cash_out` | Envio SPEI é resolvido (confirmado ou falhado) | +| `refund_in` | Refund de cash-in é processado | +| `refund_out` | Refund de cash-out é processado | + +## Verificar assinatura + +Cada webhook chega com o header `X-NTXPay-Signature` no formato `sha256=`: + +```python +import hmac +import hashlib + +def verify(payload_bytes: bytes, signature_header: str, secret: str) -> bool: + expected = "sha256=" + hmac.new( + secret.encode(), + payload_bytes, + hashlib.sha256, + ).hexdigest() + return hmac.compare_digest(expected, signature_header) +``` + +## Testar dedupe + +Cada entrega tem um `deliveryId` único no header `X-NTXPay-Delivery` e dentro do payload. Para testar seu dedupe: + +1. Configure seu handler para retornar `500` na primeira tentativa. +2. O NTX Pay vai entregar a mesma mensagem novamente (com o **mesmo** `deliveryId`). +3. Confirme que seu sistema ignora a duplicata e responde `200` na segunda tentativa. + +## Política de retry + +| Tentativa | Atraso após anterior | +|---|---| +| 1 | imediato | +| 2 | 30s | +| 3 | 2min | +| 4 | 10min | +| 5 | 1h | +| 6 | 6h | +| 7+ | desistido | + +Seu endpoint precisa responder `2xx` em até **5 segundos** — qualquer `5xx`, timeout ou erro de conexão dispara retry. + +## Cenários de teste + +Force o webhook sair como `FAILED` ou com delay: + +```bash +curl -X POST https://sandbox.ntxpay.com/api/spei/cash-out \ + -H "Authorization: Bearer $TOKEN" \ + -H "X-Sandbox-Scenario: delayed:30s" \ + -H "Content-Type: application/json" \ + -d '{ ... }' +``` + +Veja [Cenários](/pt-br/sandbox/scenarios) para o catálogo completo. + +## Boas práticas + +1. **Responda 200 antes de processar** — enfileire o evento em background; cinco segundos é o teto. +2. **Use `deliveryId` para dedupe** — não confie em `transaction.id` (retries chegam com o mesmo `transaction.id` mas `deliveryId` novo no caso de redrive manual). +3. **Não dependa da ordem** — webhooks podem chegar fora de ordem após retries. +4. **Valide a assinatura** — sempre, mesmo em sandbox.