From bb2674fabb25fe82034f56211ae0476815c7c1f7 Mon Sep 17 00:00:00 2001 From: Adson Rodrigues Date: Mon, 29 Jun 2026 23:51:04 -0300 Subject: [PATCH 1/2] docs: alinhar contrato ao backend (public-ms) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend é a fonte da verdade (whitelist + forbidNonWhitelisted). - signup: contrato novo (required email/holderName/entityType + razonSocial condicional; holderTaxIdPrimary opcional; holderTaxIdPrimaryType/accountType deprecados; isSandbox default true). - spei cash-in/out: required corretos; remove aliases Voluti (rejeitados pelo backend); CLABE somente 18 dígitos; remove externalId do cash-out. - get-started: adiciona customerEmail no exemplo de cash-in. - webhooks: events = exatamente 1. - X-Sandbox-Scenario: padroniza nos valores canônicos (corrige cash_in_failed, error:*, delayed:Ns). - openapi: adiciona paths /balance, /transactions, /webhooks-config/test; remove path fantasma /spei/transaction/{externalId}. Co-Authored-By: Claude Opus 4.8 (1M context) --- api-reference/openapi.json | 525 +++++++++++++++++++++-------- en/guides/get-started.mdx | 7 +- en/guides/quickstart.mdx | 8 +- en/guides/sandbox-testing.mdx | 5 +- en/guides/signup.mdx | 59 ++-- en/guides/spei-cash-in.mdx | 8 +- en/guides/webhooks/overview.mdx | 2 +- en/guides/webhooks/setup.mdx | 12 +- en/sandbox/cash-in.mdx | 13 +- en/sandbox/cash-out.mdx | 30 +- en/sandbox/scenarios.mdx | 45 ++- en/sandbox/webhooks.mdx | 6 +- es/guides/get-started.mdx | 7 +- es/guides/quickstart.mdx | 8 +- es/guides/sandbox-testing.mdx | 54 ++- es/guides/signup.mdx | 59 ++-- es/guides/spei-cash-in.mdx | 8 +- es/guides/webhooks/overview.mdx | 2 +- es/guides/webhooks/setup.mdx | 12 +- es/sandbox/cash-in.mdx | 13 +- es/sandbox/cash-out.mdx | 30 +- es/sandbox/scenarios.mdx | 45 ++- es/sandbox/webhooks.mdx | 6 +- pt-br/guides/get-started.mdx | 5 +- pt-br/guides/sandbox-testing.mdx | 54 ++- pt-br/guides/webhooks/overview.mdx | 2 +- pt-br/guides/webhooks/setup.mdx | 12 +- pt-br/sandbox/cash-in.mdx | 13 +- pt-br/sandbox/cash-out.mdx | 30 +- pt-br/sandbox/scenarios.mdx | 45 ++- pt-br/sandbox/webhooks.mdx | 6 +- 31 files changed, 696 insertions(+), 435 deletions(-) diff --git a/api-reference/openapi.json b/api-reference/openapi.json index 892927e..340d4ad 100644 --- a/api-reference/openapi.json +++ b/api-reference/openapi.json @@ -25,6 +25,14 @@ "name": "SPEI", "description": "Transferências interbancárias instantâneas mexicanas (cash-in via CLABE descartável; cash-out para CLABE)" }, + { + "name": "Balance", + "description": "Consulta de saldo da conta (centavos MXN)" + }, + { + "name": "Transactions", + "description": "Listagem paginada de transações da conta (rate-limit 30/min)" + }, { "name": "Webhooks Config", "description": "Configuração de webhooks para notificações de eventos" @@ -93,48 +101,61 @@ "CreateSignupInputDto": { "type": "object", "required": [ + "email", "holderName", - "holderTaxIdPrimary", - "holderTaxIdPrimaryType", - "accountType" + "entityType" ], "properties": { + "email": { + "type": "string", + "format": "email", + "description": "Email de contato — vira o login (owner) do portal.", + "example": "dev@empresa.com" + }, "holderName": { "type": "string", "minLength": 3, "maxLength": 255, - "description": "Nome do titular", - "example": "Maria Lopez" + "description": "Nome/razão do titular da conta", + "example": "Empresa Demo S.A. de C.V." + }, + "entityType": { + "type": "string", + "enum": [ + "SAPI_CV", + "SA_CV", + "OTHER" + ], + "description": "Tipo de entidade jurídica mexicana. `SAPI_CV` e `SA_CV` exigem `razonSocial`.", + "example": "OTHER" + }, + "razonSocial": { + "type": "string", + "minLength": 3, + "maxLength": 255, + "description": "Razão social. **Obrigatória** quando `entityType` é `SAPI_CV` (sufixo \"S.A.P.I. de C.V.\") ou `SA_CV` (sufixo \"S.A. de C.V.\").", + "example": "Empresa Demo S.A. de C.V." }, "holderTaxIdPrimary": { "type": "string", "minLength": 10, "maxLength": 20, - "description": "RFC (PF/PJ) ou CURP (PF)", - "example": "LOMA850101ABC" + "description": "RFC do titular (10-20 caracteres). **Opcional** no sandbox: se omitido, um RFC único de teste é gerado automaticamente.", + "example": "XAXX010101000" }, - "holderTaxIdPrimaryType": { - "type": "string", - "enum": [ - "RFC", - "CURP" - ], - "example": "RFC" - }, - "accountType": { + "name": { "type": "string", - "enum": [ - "PJ", - "PF" - ], - "description": "PJ = empresa, PF = pessoa física", - "example": "PF" + "minLength": 3, + "maxLength": 255, + "description": "Nome do contato (default = holderName).", + "example": "Maria Lopez" }, - "email": { + "holderNickname": { "type": "string", - "format": "email", - "description": "Email de contato", - "example": "maria@example.com" + "minLength": 3, + "maxLength": 100, + "description": "Apelido / nome fantasia.", + "example": "Empresa Demo" }, "phone": { "type": "string", @@ -145,8 +166,27 @@ }, "isSandbox": { "type": "boolean", - "description": "Se true, cria conta em sandbox (mainProvider=sandbox). Default false.", - "default": false + "description": "Default **true**: cria conta SANDBOX self-service. `isSandbox=false` (conta de produção) NÃO é self-service — requer onboarding com KYC pelo time.", + "default": true, + "example": true + }, + "holderTaxIdPrimaryType": { + "type": "string", + "enum": [ + "RFC", + "CURP" + ], + "deprecated": true, + "description": "[DEPRECATED] Ignorado pelo backend (o tipo do documento é sempre RFC; a classificação jurídica vem de `entityType`)." + }, + "accountType": { + "type": "string", + "enum": [ + "PJ", + "PF" + ], + "deprecated": true, + "description": "[DEPRECATED] Ignorado pelo backend. Use `entityType`." } } }, @@ -208,18 +248,23 @@ }, "SpeiCashInInputDto": { "type": "object", + "required": [ + "amountCentavos", + "customerName", + "customerEmail" + ], "properties": { "amountCentavos": { "type": "integer", "minimum": 1, - "description": "Valor em centavos MXN (mín. 1). Use ESTE campo OU `amount` (mutuamente exclusivos).", + "description": "Valor em centavos MXN (mín. 1).", "example": 50000 }, "externalId": { "type": "string", "minLength": 1, "maxLength": 100, - "description": "Identificador externo (idempotência)", + "description": "Identificador externo / referência do cliente.", "example": "order-abc-123" }, "description": { @@ -232,12 +277,13 @@ "type": "string", "minLength": 1, "maxLength": 255, - "description": "Nome do pagador", + "description": "Nome do pagador (mostrado no checkout SPEI)", "example": "Juan Perez" }, "customerEmail": { "type": "string", "format": "email", + "description": "Email do pagador", "example": "juan@example.com" }, "customerTaxId": { @@ -246,25 +292,6 @@ "maxLength": 20, "description": "RFC/CURP do pagador", "example": "PEPJ800101ABC" - }, - "amount": { - "type": "string", - "pattern": "^\\d+\\.\\d{2}$", - "description": "Alias Voluti — valor como string `\"100.00\"`. Convertido para centavos. Não usar junto com `amountCentavos`.", - "example": "500.00" - }, - "payer_name": { - "type": "string", - "maxLength": 255, - "description": "Alias Voluti de `customerName`. Nome do pagador.", - "example": "Juan Perez" - }, - "conciliationId": { - "type": "string", - "maxLength": 100, - "pattern": "^[a-zA-Z0-9_]+$", - "description": "Alias Voluti de `externalId`. Aceita apenas letras, dígitos e underscore.", - "example": "order_abc_123" } } }, @@ -327,28 +354,27 @@ "amountCentavos": { "type": "integer", "example": 50000 - }, - "conciliationId": { - "type": "string", - "description": "Alias Voluti — espelha `externalId` recebido na requisição (presente quando enviado).", - "example": "order_abc_123", - "nullable": true } } }, "SpeiCashOutInputDto": { "type": "object", + "required": [ + "amountCentavos", + "destinationClabe", + "beneficiaryName" + ], "properties": { "amountCentavos": { "type": "integer", "minimum": 1, "example": 50000, - "description": "Valor em centavos MXN (mín. 1). Use ESTE campo OU `amount`." + "description": "Valor em centavos MXN (mín. 1)." }, "destinationClabe": { "type": "string", - "pattern": "^(\\d{18}|\\d{16})$", - "description": "CLABE destino. 18 dígitos (CLABE bancária) **ou** 16 dígitos (cartão de débito). Use ESTE OU `receiverClabe`.", + "pattern": "^\\d{18}$", + "description": "CLABE destino (exatamente 18 dígitos).", "example": "012180001234567890" }, "beneficiaryName": { @@ -370,42 +396,6 @@ "maxLength": 255, "description": "Conceito/descrição (aparece para o beneficiário)", "example": "Pago factura 123" - }, - "amount": { - "type": "string", - "pattern": "^\\d+\\.\\d{2}$", - "description": "Alias Voluti — valor como string `\"100.00\"`.", - "example": "500.00" - }, - "receiverClabe": { - "type": "string", - "pattern": "^(\\d{18}|\\d{16})$", - "description": "Alias Voluti de `destinationClabe`.", - "example": "012180001234567890" - }, - "receiverName": { - "type": "string", - "maxLength": 255, - "description": "Alias Voluti de `beneficiaryName`.", - "example": "Maria Lopez" - }, - "bankCode": { - "type": "string", - "description": "Alias Voluti — código do banco. **Aceito e ignorado** (a CLABE já contém o ISPB).", - "example": "002" - }, - "conciliationId": { - "type": "string", - "maxLength": 100, - "pattern": "^[a-zA-Z0-9_]+$", - "description": "Alias Voluti de `externalId`.", - "example": "payout_001" - }, - "externalId": { - "type": "string", - "maxLength": 100, - "description": "Identificador externo / referência do cliente.", - "example": "payout-001" } } }, @@ -443,12 +433,6 @@ "nullable": true, "format": "date-time", "example": "2026-05-13T12:00:00.000Z" - }, - "conciliationId": { - "type": "string", - "description": "Alias Voluti — espelha `externalId` recebido na requisição.", - "example": "payout_001", - "nullable": true } } }, @@ -477,8 +461,7 @@ ] }, "example": [ - "cash_in", - "cash_out" + "cash_in" ] }, "isActive": { @@ -527,7 +510,8 @@ "events": { "type": "array", "minItems": 1, - "maxItems": 5, + "maxItems": 1, + "description": "Um webhook assina **exatamente UM** evento. O campo é um array por compatibilidade de contrato, mas deve conter um único item.", "items": { "type": "string", "enum": [ @@ -539,8 +523,7 @@ ] }, "example": [ - "cash_in", - "cash_out" + "cash_in" ] }, "secret": { @@ -569,8 +552,7 @@ "type": "string" }, "example": [ - "cash_in", - "cash_out" + "cash_in" ] }, "isActive": { @@ -597,23 +579,42 @@ } } }, - "SpeiTransactionOutputDto": { + "GetBalanceOutputDto": { + "type": "object", + "properties": { + "availableCentavos": { + "type": "integer", + "description": "Saldo disponível em centavos (MXN)", + "example": 4873490 + }, + "pendingCentavos": { + "type": "integer", + "description": "Saldo pendente / bloqueado em centavos (MXN)", + "example": 0 + }, + "currency": { + "type": "string", + "enum": [ + "MXN" + ], + "description": "Moeda da conta (sempre MXN)", + "example": "MXN" + } + } + }, + "TransactionOutputDto": { "type": "object", "properties": { "id": { "type": "integer", + "description": "ID interno da transação", "example": 12345 }, "externalId": { "type": "string", "nullable": true, - "example": "order-abc-123" - }, - "conciliationId": { - "type": "string", - "nullable": true, - "description": "Alias Voluti — espelha `externalId`.", - "example": "order_abc_123" + "description": "External / idempotency reference", + "example": "ext-abc-123" }, "paymentMethod": { "type": "string", @@ -624,7 +625,8 @@ "enum": [ "in", "out" - ] + ], + "example": "in" }, "type": { "type": "string", @@ -638,7 +640,8 @@ "CONFIRMED", "FAILED", "EXPIRED" - ] + ], + "example": "CONFIRMED" }, "provider": { "type": "string", @@ -651,6 +654,7 @@ "clabe": { "type": "string", "nullable": true, + "description": "CLABE de origem / destino", "example": "012180001234567890" }, "createdAt": { @@ -665,6 +669,130 @@ "example": "2026-05-12T14:31:05.000Z" } } + }, + "PaginationMetadataDto": { + "type": "object", + "properties": { + "total": { + "type": "integer", + "description": "Total de registros", + "example": 142 + }, + "limit": { + "type": "integer", + "description": "Limite aplicado", + "example": 20 + }, + "offset": { + "type": "integer", + "description": "Offset aplicado", + "example": 0 + }, + "hasMore": { + "type": "boolean", + "description": "Existe próxima página", + "example": true + } + } + }, + "SearchTransactionsOutputDto": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TransactionOutputDto" + } + }, + "metadata": { + "$ref": "#/components/schemas/PaginationMetadataDto" + } + } + }, + "TestWebhookInputDto": { + "type": "object", + "required": [ + "eventType" + ], + "properties": { + "eventType": { + "type": "string", + "enum": [ + "cash_in", + "cash_out", + "refund_in", + "refund_out", + "internal_transfer" + ], + "description": "Evento a simular (qual webhook recebe)." + }, + "status": { + "type": "string", + "enum": [ + "LIQUIDATED", + "PENDING", + "REJECTED", + "RETURNED" + ], + "default": "LIQUIDATED", + "description": "Status no payload. REJECTED/RETURNED simulam estados de ERRO." + }, + "overrideUrl": { + "type": "string", + "description": "URL temporária de teste (ex.: webhook.site). Se omitida, entrega na URL configurada. A assinatura usa o secret do webhook configurado." + }, + "amountCentavos": { + "type": "integer", + "minimum": 1, + "default": 1000, + "description": "Valor em centavos (default 1000 = $10)." + } + } + }, + "TestWebhookOutputDto": { + "type": "object", + "properties": { + "delivered": { + "type": "boolean", + "description": "true = o endpoint do integrador respondeu 2xx." + }, + "url": { + "type": "string", + "description": "URL para onde o webhook de teste foi enviado." + }, + "eventId": { + "type": "string", + "description": "ID do evento (header x-event-id) — use para idempotência." + }, + "status": { + "type": "string" + }, + "signed": { + "type": "boolean", + "description": "true = payload assinado (X-NTXPay-Signature)." + }, + "statusCode": { + "type": "integer", + "description": "Status HTTP que o endpoint do integrador respondeu (0 = erro de conexão)." + }, + "timeMs": { + "type": "integer" + }, + "signatureHeader": { + "type": "string", + "nullable": true, + "description": "Header X-NTXPay-Signature enviado (sha256=)." + }, + "payloadSent": { + "type": "object", + "description": "Payload enviado — compare com o que seu endpoint recebeu.", + "additionalProperties": true + }, + "merchantResponse": { + "nullable": true, + "description": "Trecho da resposta do seu endpoint." + } + } } } }, @@ -853,13 +981,46 @@ } } }, - "/api/spei/transaction/{externalId}": { + "/api/balance": { "get": { - "summary": "Consultar transação SPEI por externalId / conciliationId", - "description": "**Requer Bearer JWT**. Retorna UM registro SPEI da conta autenticada. Aceita o `externalId` enviado no cash-in/cash-out original. Compatível com `GET /api/v1/transaction/{conciliationId}/conciliation` da Voluti SPEI.", - "operationId": "SpeiTransactionController_get", + "summary": "Consultar saldo da conta", + "description": "**Requer Bearer JWT**. O `accountId` é extraído do token. Retorna o saldo da conta em centavos MXN.", + "operationId": "BalanceController_getBalance", "tags": [ - "SPEI" + "Balance" + ], + "security": [ + { + "bearer": [] + } + ], + "responses": { + "200": { + "description": "Saldo consultado com sucesso", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetBalanceOutputDto" + } + } + } + }, + "401": { + "description": "Token inválido ou ausente" + }, + "502": { + "description": "account-ms indisponível" + } + } + } + }, + "/api/transactions": { + "get": { + "summary": "Listar transações da conta autenticada", + "description": "**Requer Bearer JWT**. O `accountId` é extraído do token. Retorna uma lista paginada de transações da conta. **Rate-limit: 30 req/min por conta.**", + "operationId": "TransactionsController_search", + "tags": [ + "Transactions" ], "security": [ { @@ -868,32 +1029,82 @@ ], "parameters": [ { - "name": "externalId", - "in": "path", - "required": true, + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 20 + }, + "description": "Quantidade máxima de registros (1-100, default 20)" + }, + { + "name": "offset", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 0, + "default": 0 + }, + "description": "Offset para paginação (default 0)" + }, + { + "name": "status", + "in": "query", + "required": false, "schema": { "type": "string" }, - "description": "External ID enviado no cash-in/cash-out (também aceito como conciliationId)", - "example": "order-abc-123" + "description": "Filtro por status (PENDING | CONFIRMED | FAILED | EXPIRED)", + "example": "CONFIRMED" + }, + { + "name": "paymentMethod", + "in": "query", + "required": false, + "schema": { + "type": "string" + }, + "description": "Filtro por payment method (SPEI)", + "example": "SPEI" + }, + { + "name": "direction", + "in": "query", + "required": false, + "schema": { + "type": "string", + "enum": [ + "in", + "out" + ] + }, + "description": "Filtro por direction (in | out)", + "example": "in" } ], "responses": { "200": { - "description": "Transação encontrada", + "description": "Transações retornadas com sucesso", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SpeiTransactionOutputDto" + "$ref": "#/components/schemas/SearchTransactionsOutputDto" } } } }, + "400": { + "description": "Parâmetros inválidos" + }, "401": { - "description": "Token inválido" + "description": "Token inválido ou ausente" }, - "404": { - "description": "Transação não encontrada ou não pertence à conta" + "429": { + "description": "Rate limit excedido (30 req/min por conta)" }, "502": { "description": "mexico-ms indisponível" @@ -932,7 +1143,7 @@ }, "post": { "summary": "Configurar webhook", - "description": "**Requer Bearer JWT**. URL HTTPS + lista de eventos (1-5) + secret opcional para assinatura HMAC. Se `secret` for omitido, o NTX Pay gera um automaticamente e retorna na resposta — guarde com segurança.", + "description": "**Requer Bearer JWT**. URL HTTPS + exatamente UM evento (array com 1 item) + secret opcional para assinatura HMAC. Se `secret` for omitido, o NTX Pay gera um automaticamente e retorna na resposta — guarde com segurança.", "operationId": "WebhooksConfigController_setup", "tags": [ "Webhooks Config" @@ -975,6 +1186,52 @@ } } }, + "/api/webhooks-config/test": { + "post": { + "summary": "Enviar webhook de teste", + "description": "**Requer Bearer JWT**. Dispara um webhook de teste assinado (`X-NTXPay-Signature`) no webhook configurado para o `eventType` escolhido — ou numa `overrideUrl` (ex.: webhook.site). Use `status` `REJECTED`/`RETURNED` para simular estados de ERRO. One-shot (sem retry).", + "operationId": "WebhooksConfigController_test", + "tags": [ + "Webhooks Config" + ], + "security": [ + { + "bearer": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TestWebhookInputDto" + } + } + } + }, + "responses": { + "200": { + "description": "Resultado da entrega (delivered, statusCode, payloadSent, assinatura).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TestWebhookOutputDto" + } + } + } + }, + "400": { + "description": "Nenhum webhook configurado p/ o evento, ou payload inválido" + }, + "401": { + "description": "Token inválido" + }, + "502": { + "description": "notification-ms indisponível" + } + } + } + }, "/api/webhooks-config/{id}": { "delete": { "summary": "Remover webhook", diff --git a/en/guides/get-started.mdx b/en/guides/get-started.mdx index 9071aa6..c3d1fae 100644 --- a/en/guides/get-started.mdx +++ b/en/guides/get-started.mdx @@ -25,7 +25,7 @@ The NTX Pay México API lets your company perform payment operations, check bala -H "Content-Type: application/json" \ -d '{ "url": "https://my-server.com/webhooks/ntxpay", - "events": ["cash_in", "cash_out"] + "events": ["cash_in"] }' ``` @@ -38,12 +38,13 @@ The NTX Pay México API lets your company perform payment operations, check bala ```bash curl -X POST https://sandbox.mx.ntxpay.com/api/spei/cash-in \ -H "Authorization: Bearer $TOKEN" \ - -H "X-Sandbox-Scenario: cash_in_failed" \ + -H "X-Sandbox-Scenario: rejected" \ -H "Content-Type: application/json" \ -d '{ "amountCentavos": 50000, "externalId": "order-001", - "customerName": "Juan Perez" + "customerName": "Juan Perez", + "customerEmail": "juan@example.com" }' ``` diff --git a/en/guides/quickstart.mdx b/en/guides/quickstart.mdx index f46c5bf..edd40e0 100644 --- a/en/guides/quickstart.mdx +++ b/en/guides/quickstart.mdx @@ -15,11 +15,9 @@ description: 'Signup, authentication and first SPEI transaction' curl -X POST https://sandbox.mx.ntxpay.com/api/signup \ -H "Content-Type: application/json" \ -d '{ - "holderName": "Maria Lopez", - "holderTaxIdPrimary": "LOMA850101ABC", - "holderTaxIdPrimaryType": "RFC", - "accountType": "PF", "email": "maria@example.com", + "holderName": "Maria Lopez", + "entityType": "OTHER", "isSandbox": true }' ``` @@ -47,7 +45,7 @@ curl -X POST https://sandbox.mx.ntxpay.com/api/webhooks-config \ -H "Content-Type: application/json" \ -d '{ "url": "https://my-server.com/webhooks/ntxpay", - "events": ["cash_in", "cash_out"] + "events": ["cash_in"] }' ``` diff --git a/en/guides/sandbox-testing.mdx b/en/guides/sandbox-testing.mdx index 59a407f..85aebcf 100644 --- a/en/guides/sandbox-testing.mdx +++ b/en/guides/sandbox-testing.mdx @@ -19,10 +19,9 @@ The **sandbox** is an isolated environment with simulated transactions. Useful f curl -X POST https://sandbox.mx.ntxpay.com/api/signup \ -H "Content-Type: application/json" \ -d '{ + "email": "dev@example.com", "holderName": "Test", - "holderTaxIdPrimary": "TEST850101ABC", - "holderTaxIdPrimaryType": "RFC", - "accountType": "PF", + "entityType": "OTHER", "isSandbox": true }' ``` diff --git a/en/guides/signup.mdx b/en/guides/signup.mdx index 6c2503a..8269c7b 100644 --- a/en/guides/signup.mdx +++ b/en/guides/signup.mdx @@ -12,8 +12,8 @@ On creation, NTX Pay: 1. Sets up the account in Keycloak (identity provider) 2. Applies default limits and fees 3. Sets the `mainProvider`: - - `sandbox` if `isSandbox=true` (simulated transactions) - - `smartfastpay` otherwise (production) + - `sandbox` if `isSandbox=true` (default — self-service simulated transactions) + - `smartfastpay` otherwise (production — **not** self-service; requires KYC onboarding with the team) 4. Issues OAuth 2.0 credentials (`clientId` + `clientSecret`) 5. Optionally issues mTLS certificate @@ -27,27 +27,30 @@ On creation, NTX Pay: curl -X POST https://sandbox.mx.ntxpay.com/api/signup \ -H "Content-Type: application/json" \ -d '{ - "holderName": "Maria Lopez", - "holderTaxIdPrimary": "LOMA850101ABC", - "holderTaxIdPrimaryType": "RFC", - "accountType": "PF", "email": "maria@example.com", + "holderName": "Maria Lopez", + "entityType": "OTHER", "phone": "+5215512345678", "isSandbox": true }' ``` -#### Request — Legal Entity (production) + + In sandbox, `holderTaxIdPrimary` (RFC) is optional — if omitted, a unique test RFC is generated automatically. + + +#### Request — Legal Entity (`SA_CV` / `SAPI_CV`) ```bash curl -X POST https://sandbox.mx.ntxpay.com/api/signup \ -H "Content-Type: application/json" \ -d '{ + "email": "contact@acme.mx", "holderName": "Acme S.A. de C.V.", + "entityType": "SA_CV", + "razonSocial": "Acme S.A. de C.V.", "holderTaxIdPrimary": "ACM850101AB1", - "holderTaxIdPrimaryType": "RFC", - "accountType": "PJ", - "email": "contact@acme.mx" + "isSandbox": true }' ``` @@ -76,33 +79,41 @@ curl -X POST https://sandbox.mx.ntxpay.com/api/signup \ ## Request Fields - - Full name of the holder (minimum 3, maximum 255 characters). + + Contact email — becomes the portal login (owner). - - RFC (Individual or Legal Entity) or CURP (Individual). 10–20 characters. + + Name / legal name of the account holder (minimum 3, maximum 255 characters). - - `RFC` or `CURP`. + + Mexican legal entity type: `SAPI_CV`, `SA_CV` or `OTHER`. `SAPI_CV` and `SA_CV` require `razonSocial`. - - `PJ` (legal entity) or `PF` (individual). + + Legal name (razón social). **Required** when `entityType` is `SAPI_CV` (suffix "S.A.P.I. de C.V.") or `SA_CV` (suffix "S.A. de C.V."). Optional for `OTHER`. - - Contact email. Optional, but recommended. + + RFC of the holder (10–20 characters). **Optional in sandbox**: if omitted, a unique test RFC is generated automatically. Phone in E.164 format (8–20 characters). Ex.: `+5215512345678`. - - `true` → sandbox environment account (provider `sandbox`, simulated transactions). - `false` → production account (provider `smartfastpay`). + + Defaults to **`true`** → self-service sandbox account (provider `sandbox`, simulated transactions). + `false` → production account (provider `smartfastpay`) — **not** self-service; requires KYC onboarding with the team. + + + + **Deprecated** — accepted but ignored by the backend (the document type is always RFC). Legal classification comes from `entityType`. + + + + **Deprecated** — accepted but ignored by the backend. Use `entityType` instead. ## Sandbox vs Production @@ -119,7 +130,7 @@ curl -X POST https://sandbox.mx.ntxpay.com/api/signup \ | Code | Cause | |---|---| -| `400` | Invalid RFC/CURP, invalid accountType, or tax ID already in use | +| `400` | Missing required field (`email`/`holderName`/`entityType`), missing `razonSocial` for `SAPI_CV`/`SA_CV`, invalid RFC, or tax ID already in use | | `502` | `account-ms` (provisioning) unavailable — retry | ## Next Steps diff --git a/en/guides/spei-cash-in.mdx b/en/guides/spei-cash-in.mdx index 4d8d165..13fbc69 100644 --- a/en/guides/spei-cash-in.mdx +++ b/en/guides/spei-cash-in.mdx @@ -72,12 +72,12 @@ curl -X POST https://sandbox.mx.ntxpay.com/api/spei/cash-in \ Charge description (up to 255 characters). - - Payer name (up to 255 characters). Useful for reconciliation. + + Payer name (1–255 characters), shown on the SPEI checkout. - - Payer email. If provided along with `checkoutUrl`, can trigger automatic link sending. + + Payer email (valid email format). diff --git a/en/guides/webhooks/overview.mdx b/en/guides/webhooks/overview.mdx index 191d4d7..7867ed6 100644 --- a/en/guides/webhooks/overview.mdx +++ b/en/guides/webhooks/overview.mdx @@ -22,7 +22,7 @@ Webhooks let NTX Pay send HTTPS notifications to your server whenever a relevant Endpoint: `POST /api/webhooks-config`. You provide: - **`url`** — HTTPS endpoint on your server -- **`events`** — array with 1 to 5 events +- **`events`** — array with exactly 1 event (one webhook subscribes to one event) - **`secret`** — optional. If omitted, NTX Pay generates one automatically and returns it in the response. See the [Setup guide](/en/guides/webhooks/setup) for the step-by-step. diff --git a/en/guides/webhooks/setup.mdx b/en/guides/webhooks/setup.mdx index a66cc9b..4a84c42 100644 --- a/en/guides/webhooks/setup.mdx +++ b/en/guides/webhooks/setup.mdx @@ -21,7 +21,7 @@ curl -X POST https://sandbox.mx.ntxpay.com/api/webhooks-config \ -H "Content-Type: application/json" \ -d '{ "url": "https://my-server.com/webhooks/ntxpay", - "events": ["cash_in", "cash_out"], + "events": ["cash_in"], "secret": "whsec_abc123def456" }' ``` @@ -32,7 +32,7 @@ curl -X POST https://sandbox.mx.ntxpay.com/api/webhooks-config \ { "id": 42, "url": "https://my-server.com/webhooks/ntxpay", - "events": ["cash_in", "cash_out"], + "events": ["cash_in"], "isActive": true, "secret": "whsec_abc123def456" } @@ -49,7 +49,7 @@ curl -X POST https://sandbox.mx.ntxpay.com/api/webhooks-config \ - List of 1 to 5 events. Accepted values: `cash_in`, `cash_out`, `refund_in`, `refund_out`, `internal_transfer`. + A webhook subscribes to **exactly ONE** event — the array must contain a single item. Accepted values: `cash_in`, `cash_out`, `refund_in`, `refund_out`, `internal_transfer`. To receive more than one event type, create one webhook per event. @@ -70,7 +70,7 @@ curl -X GET https://sandbox.mx.ntxpay.com/api/webhooks-config \ { "id": 42, "url": "https://my-server.com/webhooks/ntxpay", - "events": ["cash_in", "cash_out"], + "events": ["cash_in"], "isActive": true, "createdAt": "2026-05-01T10:30:00.000Z" } @@ -99,9 +99,9 @@ curl -X DELETE https://sandbox.mx.ntxpay.com/api/webhooks-config/42 \ ## Multiple Webhooks -You can configure **multiple webhooks** at the same time, each with its own event set. Useful for: +Each webhook subscribes to exactly one event, so configure **one webhook per event type** you want to receive (e.g. one for `cash_in`, another for `cash_out`). Useful for: -- Separating **logs/audit** (receives all events) from **processing** (only `cash_in`/`cash_out`) +- Routing each event type to its own endpoint/handler - Internal **homologation** vs production environment - Multiple services consuming different events diff --git a/en/sandbox/cash-in.mdx b/en/sandbox/cash-in.mdx index d2d8f69..f5dca3a 100644 --- a/en/sandbox/cash-in.mdx +++ b/en/sandbox/cash-in.mdx @@ -19,7 +19,8 @@ curl -X POST https://sandbox.mx.ntxpay.com/api/spei/cash-in \ -d '{ "amountCentavos": 50000, "externalId": "order-001", - "customerName": "Juan Pérez" + "customerName": "Juan Pérez", + "customerEmail": "juan@example.com" }' ``` @@ -69,10 +70,12 @@ After ~1 second (default `success` scenario): | Scenario | Webhook | |---|---| -| Default | `CONFIRMED` | -| `error:invalid-clabe` | `FAILED` | -| `error:duplicate-external-id` | `FAILED` | -| `delayed:5s` | `CONFIRMED` after 5s | +| `success` (default) | `CONFIRMED` | +| `pending_long` | `CONFIRMED` after ~30s | +| `rejected` | `FAILED` | +| `returned` | `RETURNED` | + +`timeout` and `provider_5xx` fail on the synchronous HTTP response. `insufficient_funds` and `bad_clabe` do **not** apply to cash-in (they return `400 SCENARIO_NOT_APPLICABLE`). See [Scenarios](/en/sandbox/scenarios) for the full list. diff --git a/en/sandbox/cash-out.mdx b/en/sandbox/cash-out.mdx index 23a6d00..97a1bb8 100644 --- a/en/sandbox/cash-out.mdx +++ b/en/sandbox/cash-out.mdx @@ -25,8 +25,7 @@ curl -X POST https://sandbox.mx.ntxpay.com/api/spei/cash-out \ "destinationClabe": "012180001234567890", "beneficiaryName": "Maria Lopez", "beneficiaryTaxId": "LOPM850101ABC", - "externalId": "payout-001", - "description": "Supplier payment" + "concept": "Supplier payment" }' ``` @@ -35,10 +34,11 @@ curl -X POST https://sandbox.mx.ntxpay.com/api/spei/cash-out \ ```json { "id": 12346, - "externalId": "payout-001", "status": "PENDING", + "destinationClabe": "012180001234567890", "amountCentavos": 15000, - "clabe": "012180001234567890" + "referenceNumerical": "9876543", + "createdAt": "2026-03-26T10:00:00.000Z" } ``` @@ -71,18 +71,19 @@ Force specific behaviors via the `X-Sandbox-Scenario` header: ```bash curl -X POST https://sandbox.mx.ntxpay.com/api/spei/cash-out \ -H "Authorization: Bearer $TOKEN" \ - -H "X-Sandbox-Scenario: error:insufficient-funds" \ + -H "X-Sandbox-Scenario: 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 | +| Scenario | When to use | Webhook | +|---|---|---| +| `insufficient_funds` | Validate UX when your customer tries to pay without balance | `FAILED` (`errorCode: INSUFFICIENT_FUNDS`) | +| `bad_clabe` | Validate handling of a transfer returned for an invalid CLABE | `RETURNED` (`errorCode: INVALID_CLABE`) | +| `rejected` | Validate generic provider/network rejection | `FAILED` | +| `returned` | Validate a transfer reversed by the counterpart bank | `RETURNED` | +| `pending_long` | Validate "transfer in progress" UX (~30s) | `CONFIRMED` | +| `timeout` / `provider_5xx` | Validate synchronous upstream failures (`504` / `503`) | — (synchronous error) | See [Scenarios](/en/sandbox/scenarios) for the full list. @@ -93,10 +94,9 @@ 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 INVALID_CLABE_FORMAT` | `400` | CLABE with invalid format (not exactly 18 digits) | | `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. + The `insufficient_funds`, `bad_clabe`, `rejected` and `returned` scenarios affect the **webhook** — the synchronous response is `201 PENDING`. `timeout` and `provider_5xx` fail synchronously, as do structural validations (amount/CLABE format). diff --git a/en/sandbox/scenarios.mdx b/en/sandbox/scenarios.mdx index 8f66bad..c342523 100644 --- a/en/sandbox/scenarios.mdx +++ b/en/sandbox/scenarios.mdx @@ -11,50 +11,47 @@ Add the header `X-Sandbox-Scenario: ` to any cash-in or cash-out call. ```bash curl -X POST https://sandbox.mx.ntxpay.com/api/spei/cash-out \ -H "Authorization: Bearer $TOKEN" \ - -H "X-Sandbox-Scenario: error:insufficient-funds" \ + -H "X-Sandbox-Scenario: insufficient_funds" \ -H "Content-Type: application/json" \ -d '{ "amountCentavos": 15000, "destinationClabe": "012180001234567890", - "beneficiaryName": "Maria Lopez", - "externalId": "test-error-001" + "beneficiaryName": "Maria Lopez" }' ``` - 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. + Most scenarios control the **asynchronous webhook**: the HTTP response is `201 Created` with `status: PENDING`, and the final outcome (`CONFIRMED`, `FAILED`, or `RETURNED`) arrives in the webhook. The exceptions are `timeout` and `provider_5xx`, which fail on the **synchronous** HTTP response itself. ## Available scenarios -### Success scenarios +The canonical scenario values are: `success`, `pending_long`, `rejected`, `returned`, `insufficient_funds`, `bad_clabe`, `timeout`, `provider_5xx`. -| Header Value | Synchronous behavior | Webhook | -|---|---|---| -| `success` (default) | `201 PENDING` | `CONFIRMED` in ~1s | -| _(no header)_ | `201 PENDING` | `CONFIRMED` in ~1s | +### Asynchronous outcome scenarios -### Error scenarios +These return `201 PENDING` synchronously; the final state arrives via webhook. -| Header Value | Synchronous behavior | Webhook | +| Header Value | Webhook outcome | Notes | |---|---|---| -| `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` | +| `success` (default) | `CONFIRMED` in ~1s | Also used when no header is sent | +| `pending_long` | `CONFIRMED` after ~30s | Tests slow settlement | +| `rejected` | `FAILED` | Provider / SPEI network rejected the transfer | +| `returned` | `RETURNED` | Accepted, then reversed by the counterpart bank | +| `insufficient_funds` | `FAILED` with `errorCode: INSUFFICIENT_FUNDS` | **Cash-out only** | +| `bad_clabe` | `RETURNED` with `errorCode: INVALID_CLABE` | **Cash-out only** — accepted, then returned | -### Delay scenarios +### Synchronous error 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 | +These fail on the HTTP response itself — no webhook is sent. + +| Header Value | Synchronous response | +|---|---| +| `timeout` | Upstream timeout (`504`) after ~16s | +| `provider_5xx` | `503` provider unavailable | - The maximum allowed delay is **120 seconds** — higher values are truncated automatically. + **Cash-in restrictions:** `insufficient_funds` and `bad_clabe` do not apply to cash-in (there's no balance to debit, and the deposit CLABE is system-generated). Sending either on a cash-in returns `400` with code `SCENARIO_NOT_APPLICABLE`. ## Example: success webhook diff --git a/en/sandbox/webhooks.mdx b/en/sandbox/webhooks.mdx index a90f248..6d5d69e 100644 --- a/en/sandbox/webhooks.mdx +++ b/en/sandbox/webhooks.mdx @@ -23,7 +23,7 @@ curl -X POST https://sandbox.mx.ntxpay.com/api/webhooks-config \ -H "Content-Type: application/json" \ -d '{ "url": "https://my-server.com/webhooks/ntxpay", - "events": ["cash_in", "cash_out"] + "events": ["cash_in"] }' ``` @@ -33,7 +33,7 @@ curl -X POST https://sandbox.mx.ntxpay.com/api/webhooks-config \ { "id": "wh_550e8400", "url": "https://my-server.com/webhooks/ntxpay", - "events": ["cash_in", "cash_out"], + "events": ["cash_in"], "secret": "whsec_a1b2c3d4...", "createdAt": "2026-03-26T09:00:00.000Z" } @@ -96,7 +96,7 @@ Force the webhook to come back as `FAILED` or with delay: ```bash curl -X POST https://sandbox.mx.ntxpay.com/api/spei/cash-out \ -H "Authorization: Bearer $TOKEN" \ - -H "X-Sandbox-Scenario: delayed:30s" \ + -H "X-Sandbox-Scenario: pending_long" \ -H "Content-Type: application/json" \ -d '{ ... }' ``` diff --git a/es/guides/get-started.mdx b/es/guides/get-started.mdx index d98add3..9582408 100644 --- a/es/guides/get-started.mdx +++ b/es/guides/get-started.mdx @@ -25,7 +25,7 @@ La API NTX Pay México permite que tu empresa realice operaciones de pago, consu -H "Content-Type: application/json" \ -d '{ "url": "https://mi-servidor.com/webhooks/ntxpay", - "events": ["cash_in", "cash_out"] + "events": ["cash_in"] }' ``` @@ -38,12 +38,13 @@ La API NTX Pay México permite que tu empresa realice operaciones de pago, consu ```bash curl -X POST https://sandbox.mx.ntxpay.com/api/spei/cash-in \ -H "Authorization: Bearer $TOKEN" \ - -H "X-Sandbox-Scenario: cash_in_failed" \ + -H "X-Sandbox-Scenario: rejected" \ -H "Content-Type: application/json" \ -d '{ "amountCentavos": 50000, "externalId": "order-001", - "customerName": "Juan Perez" + "customerName": "Juan Perez", + "customerEmail": "juan@example.com" }' ``` diff --git a/es/guides/quickstart.mdx b/es/guides/quickstart.mdx index fe1abb5..fa179b2 100644 --- a/es/guides/quickstart.mdx +++ b/es/guides/quickstart.mdx @@ -15,11 +15,9 @@ description: 'Signup, autenticación y primera transacción SPEI' curl -X POST https://sandbox.mx.ntxpay.com/api/signup \ -H "Content-Type: application/json" \ -d '{ - "holderName": "Maria Lopez", - "holderTaxIdPrimary": "LOMA850101ABC", - "holderTaxIdPrimaryType": "RFC", - "accountType": "PF", "email": "maria@example.com", + "holderName": "Maria Lopez", + "entityType": "OTHER", "isSandbox": true }' ``` @@ -47,7 +45,7 @@ curl -X POST https://sandbox.mx.ntxpay.com/api/webhooks-config \ -H "Content-Type: application/json" \ -d '{ "url": "https://mi-servidor.com/webhooks/ntxpay", - "events": ["cash_in", "cash_out"] + "events": ["cash_in"] }' ``` diff --git a/es/guides/sandbox-testing.mdx b/es/guides/sandbox-testing.mdx index 994d212..f0a3701 100644 --- a/es/guides/sandbox-testing.mdx +++ b/es/guides/sandbox-testing.mdx @@ -54,14 +54,15 @@ El sandbox es un **modo de operación** de tu cuenta — no un ambiente separado ## How It Works -El header **no altera la respuesta HTTP**. La API siempre responde `201 Created` con `status: "PENDING"`. El scenario afecta exclusivamente el **webhook asíncrono** disparado ~1 segundo después. +En la mayoría de los escenarios el header **no altera la respuesta HTTP**: la API responde `201 Created` con `status: "PENDING"` y el scenario afecta el **webhook asíncrono** disparado ~1 segundo después. Las excepciones son `timeout` y `provider_5xx`, que fallan en la propia respuesta HTTP síncrona. | Request | HTTP Response | Webhook (~1s después) | |---|---|---| | Sin `X-Sandbox-Scenario` | `201 PENDING` | `CONFIRMED` | | `X-Sandbox-Scenario: success` | `201 PENDING` | `CONFIRMED` | -| `X-Sandbox-Scenario: error:insufficient-funds` | `201 PENDING` | `FAILED` con `errorCode` | -| `X-Sandbox-Scenario: delayed:5s` | `201 PENDING` | `CONFIRMED` con +5s de delay | +| `X-Sandbox-Scenario: insufficient_funds` | `201 PENDING` | `FAILED` con `errorCode` (solo cash-out) | +| `X-Sandbox-Scenario: pending_long` | `201 PENDING` | `CONFIRMED` tras ~30s | +| `X-Sandbox-Scenario: provider_5xx` | `503` (síncrono) | — | ## How to Use @@ -70,13 +71,12 @@ El header **no altera la respuesta HTTP**. La API siempre responde `201 Created` ```bash cURL curl -X POST https://sandbox.mx.ntxpay.com/api/spei/cash-out \ -H "Authorization: Bearer $TOKEN" \ - -H "X-Sandbox-Scenario: error:insufficient-funds" \ + -H "X-Sandbox-Scenario: insufficient_funds" \ -H "Content-Type: application/json" \ -d '{ "amountCentavos": 15000, "destinationClabe": "012180001234567890", - "beneficiaryName": "Maria Lopez", - "externalId": "test-error-001" + "beneficiaryName": "Maria Lopez" }' ``` @@ -87,12 +87,11 @@ const response = await axios.post( amountCentavos: 15000, destinationClabe: '012180001234567890', beneficiaryName: 'Maria Lopez', - externalId: 'test-error-001', }, { headers: { Authorization: `Bearer ${token}`, - 'X-Sandbox-Scenario': 'error:insufficient-funds', + 'X-Sandbox-Scenario': 'insufficient_funds', }, } ); @@ -105,11 +104,10 @@ response = requests.post( "amountCentavos": 15000, "destinationClabe": "012180001234567890", "beneficiaryName": "Maria Lopez", - "externalId": "test-error-001", }, headers={ "Authorization": f"Bearer {token}", - "X-Sandbox-Scenario": "error:insufficient-funds", + "X-Sandbox-Scenario": "insufficient_funds", }, ) ``` @@ -118,34 +116,28 @@ response = requests.post( ## Available Scenarios -### Error Scenarios +Valores canónicos: `success`, `pending_long`, `rejected`, `returned`, `insufficient_funds`, `bad_clabe`, `timeout`, `provider_5xx`. -| Header Value | Descripción | Webhook Status | -|---|---|---| -| `error:insufficient-funds` | Cuenta sin saldo suficiente | `FAILED` | -| `error:invalid-clabe` | CLABE de destino malformada o inexistente | `FAILED` | -| `error:account-not-found` | Cuenta destino no localizada en la red SPEI | `FAILED` | -| `error:account-blocked` | Cuenta destino bloqueada o cerrada | `FAILED` | -| `error:duplicate-external-id` | `externalId` ya usado en otra transacción | `FAILED` | -| `error:bank-rejected` | Banco destino rechazó la transferencia (genérico) | `FAILED` | - -### Success Scenario +### Escenarios de resultado asíncrono (`201 PENDING` → webhook) | Header Value | Descripción | Webhook Status | |---|---|---| -| `success` | Fuerza éxito (mismo comportamiento del default) | `CONFIRMED` | -| _(sin header)_ | Comportamiento default del sandbox | `CONFIRMED` | +| `success` _(o sin header)_ | Comportamiento default del sandbox | `CONFIRMED` | +| `pending_long` | Confirma tras ~30s (settlement lento) | `CONFIRMED` | +| `rejected` | El proveedor / red SPEI rechazó | `FAILED` | +| `returned` | Aceptada y luego devuelta por el banco contraparte | `RETURNED` | +| `insufficient_funds` | Cuenta sin saldo suficiente (**solo cash-out**) | `FAILED` (`errorCode: INSUFFICIENT_FUNDS`) | +| `bad_clabe` | CLABE inválida (**solo cash-out**) | `RETURNED` (`errorCode: INVALID_CLABE`) | -### Delay Scenarios +### Escenarios de error síncrono (sin webhook) -| Header Value | Descripción | Webhook Status | +| Header Value | Descripción | HTTP Response | |---|---|---| -| `delayed:5s` | Éxito tras 5 segundos extra | `CONFIRMED` | -| `delayed:30s` | Éxito tras 30 segundos extra | `CONFIRMED` | -| `delayed:60s` | Éxito tras 60 segundos extra | `CONFIRMED` | +| `timeout` | Timeout upstream tras ~16s | `504` | +| `provider_5xx` | Proveedor no disponible | `503` | - El delay máximo permitido es de **120 segundos** — valores arriba se truncan automáticamente. + `insufficient_funds` y `bad_clabe` **no aplican a cash-in** — enviarlos en un cash-in devuelve `400` con código `SCENARIO_NOT_APPLICABLE`. ## Received Webhook Examples @@ -182,7 +174,7 @@ response = requests.post( } ``` -### Error Webhook (`error:insufficient-funds`) +### Error Webhook (`insufficient_funds`) ```json { @@ -234,7 +226,7 @@ Notas: - Tu sistema recibe el webhook con `status` correspondiente al scenario (`CONFIRMED` o `FAILED`). - El header controla **solo el webhook**. La respuesta HTTP siempre es `201 Created` con `status: PENDING`, sin importar el scenario. + En la mayoría de los escenarios el header controla **solo el webhook**, y la respuesta HTTP es `201 Created` con `status: PENDING`. Las excepciones `timeout` (`504`) y `provider_5xx` (`503`) fallan en la propia respuesta síncrona. ## Restrictions diff --git a/es/guides/signup.mdx b/es/guides/signup.mdx index f4dba80..a31926d 100644 --- a/es/guides/signup.mdx +++ b/es/guides/signup.mdx @@ -12,8 +12,8 @@ Al crearla, NTX Pay: 1. Configura la cuenta en Keycloak (identity provider) 2. Aplica límites y tarifas default 3. Define el `mainProvider`: - - `sandbox` si `isSandbox=true` (transacciones simuladas) - - `smartfastpay` en caso contrario (producción) + - `sandbox` si `isSandbox=true` (default — self-service con transacciones simuladas) + - `smartfastpay` en caso contrario (producción — **no** es self-service; requiere onboarding con KYC por el equipo) 4. Emite credenciales OAuth 2.0 (`clientId` + `clientSecret`) 5. Opcionalmente emite certificado mTLS @@ -27,27 +27,30 @@ Al crearla, NTX Pay: curl -X POST https://sandbox.mx.ntxpay.com/api/signup \ -H "Content-Type: application/json" \ -d '{ - "holderName": "Maria Lopez", - "holderTaxIdPrimary": "LOMA850101ABC", - "holderTaxIdPrimaryType": "RFC", - "accountType": "PF", "email": "maria@example.com", + "holderName": "Maria Lopez", + "entityType": "OTHER", "phone": "+5215512345678", "isSandbox": true }' ``` -#### Request — Persona Moral (producción) + + En sandbox, `holderTaxIdPrimary` (RFC) es opcional — si se omite, se genera automáticamente un RFC de prueba único. + + +#### Request — Persona Moral (`SA_CV` / `SAPI_CV`) ```bash curl -X POST https://sandbox.mx.ntxpay.com/api/signup \ -H "Content-Type: application/json" \ -d '{ + "email": "contacto@acme.mx", "holderName": "Acme S.A. de C.V.", + "entityType": "SA_CV", + "razonSocial": "Acme S.A. de C.V.", "holderTaxIdPrimary": "ACM850101AB1", - "holderTaxIdPrimaryType": "RFC", - "accountType": "PJ", - "email": "contacto@acme.mx" + "isSandbox": true }' ``` @@ -76,33 +79,41 @@ curl -X POST https://sandbox.mx.ntxpay.com/api/signup \ ## Campos del Request - - Nombre completo del titular (mínimo 3, máximo 255 caracteres). + + Email de contacto — se convierte en el login (owner) del portal. - - RFC (Persona Física o Moral) o CURP (Persona Física). 10–20 caracteres. + + Nombre / razón del titular de la cuenta (mínimo 3, máximo 255 caracteres). - - `RFC` o `CURP`. + + Tipo de entidad jurídica mexicana: `SAPI_CV`, `SA_CV` u `OTHER`. `SAPI_CV` y `SA_CV` exigen `razonSocial`. - - `PJ` (persona moral) o `PF` (persona física). + + Razón social. **Obligatoria** cuando `entityType` es `SAPI_CV` (sufijo "S.A.P.I. de C.V.") o `SA_CV` (sufijo "S.A. de C.V."). Opcional para `OTHER`. - - Email de contacto. Opcional, pero recomendado. + + RFC del titular (10–20 caracteres). **Opcional en sandbox**: si se omite, se genera automáticamente un RFC de prueba único. Teléfono en formato E.164 (8–20 caracteres). Ej.: `+5215512345678`. - - `true` → cuenta en ambiente sandbox (provider `sandbox`, transacciones simuladas). - `false` → cuenta de producción (provider `smartfastpay`). + + Default **`true`** → cuenta sandbox self-service (provider `sandbox`, transacciones simuladas). + `false` → cuenta de producción (provider `smartfastpay`) — **no** es self-service; requiere onboarding con KYC por el equipo. + + + + **Deprecado** — aceptado pero ignorado por el backend (el tipo de documento es siempre RFC). La clasificación jurídica viene de `entityType`. + + + + **Deprecado** — aceptado pero ignorado por el backend. Usa `entityType`. ## Sandbox vs Producción @@ -119,7 +130,7 @@ curl -X POST https://sandbox.mx.ntxpay.com/api/signup \ | Código | Causa | |---|---| -| `400` | RFC/CURP inválido, accountType inválido, o tax ID ya en uso | +| `400` | Falta un campo obligatorio (`email`/`holderName`/`entityType`), falta `razonSocial` para `SAPI_CV`/`SA_CV`, RFC inválido, o tax ID ya en uso | | `502` | `account-ms` (provisión) no disponible — reintentar | ## Próximos Pasos diff --git a/es/guides/spei-cash-in.mdx b/es/guides/spei-cash-in.mdx index 096c8d2..83b0265 100644 --- a/es/guides/spei-cash-in.mdx +++ b/es/guides/spei-cash-in.mdx @@ -72,12 +72,12 @@ curl -X POST https://sandbox.mx.ntxpay.com/api/spei/cash-in \ Descripción del cobro (hasta 255 caracteres). - - Nombre del pagador (hasta 255 caracteres). Útil para conciliación. + + Nombre del pagador (1–255 caracteres), mostrado en el checkout SPEI. - - Email del pagador. Si se proporciona junto con `checkoutUrl`, puede activar envío automático del link. + + Email del pagador (formato de email válido). diff --git a/es/guides/webhooks/overview.mdx b/es/guides/webhooks/overview.mdx index c7e4090..5942a18 100644 --- a/es/guides/webhooks/overview.mdx +++ b/es/guides/webhooks/overview.mdx @@ -22,7 +22,7 @@ Los webhooks permiten que NTX Pay envíe notificaciones HTTPS a tu servidor siem Endpoint: `POST /api/webhooks-config`. Proporcionas: - **`url`** — endpoint HTTPS en tu servidor -- **`events`** — array con 1 a 5 eventos +- **`events`** — array con exactamente 1 evento (un webhook se suscribe a un evento) - **`secret`** — opcional. Si se omite, NTX Pay genera uno automáticamente y lo retorna en la respuesta. Ver el [guía de Setup](/es/guides/webhooks/setup) para el paso a paso. diff --git a/es/guides/webhooks/setup.mdx b/es/guides/webhooks/setup.mdx index ed5f935..7698ce4 100644 --- a/es/guides/webhooks/setup.mdx +++ b/es/guides/webhooks/setup.mdx @@ -21,7 +21,7 @@ curl -X POST https://sandbox.mx.ntxpay.com/api/webhooks-config \ -H "Content-Type: application/json" \ -d '{ "url": "https://mi-servidor.com/webhooks/ntxpay", - "events": ["cash_in", "cash_out"], + "events": ["cash_in"], "secret": "whsec_abc123def456" }' ``` @@ -32,7 +32,7 @@ curl -X POST https://sandbox.mx.ntxpay.com/api/webhooks-config \ { "id": 42, "url": "https://mi-servidor.com/webhooks/ntxpay", - "events": ["cash_in", "cash_out"], + "events": ["cash_in"], "isActive": true, "secret": "whsec_abc123def456" } @@ -49,7 +49,7 @@ curl -X POST https://sandbox.mx.ntxpay.com/api/webhooks-config \ - Lista de 1 a 5 eventos. Valores aceptados: `cash_in`, `cash_out`, `refund_in`, `refund_out`, `internal_transfer`. + Un webhook se suscribe a **exactamente UN** evento — el array debe contener un único item. Valores aceptados: `cash_in`, `cash_out`, `refund_in`, `refund_out`, `internal_transfer`. Para recibir más de un tipo de evento, crea un webhook por evento. @@ -70,7 +70,7 @@ curl -X GET https://sandbox.mx.ntxpay.com/api/webhooks-config \ { "id": 42, "url": "https://mi-servidor.com/webhooks/ntxpay", - "events": ["cash_in", "cash_out"], + "events": ["cash_in"], "isActive": true, "createdAt": "2026-05-01T10:30:00.000Z" } @@ -99,9 +99,9 @@ curl -X DELETE https://sandbox.mx.ntxpay.com/api/webhooks-config/42 \ ## Múltiples Webhooks -Puedes configurar **varios webhooks** simultáneamente, cada uno con su conjunto de eventos. Útil para: +Cada webhook se suscribe a exactamente un evento, así que configura **un webhook por tipo de evento** que quieras recibir (p. ej. uno para `cash_in`, otro para `cash_out`). Útil para: -- Separar **logs/auditoría** (recibe todos los eventos) de **procesamiento** (solo `cash_in`/`cash_out`) +- Enrutar cada tipo de evento a su propio endpoint/handler - Ambiente de **homologación interna** vs producción - Múltiples servicios consumiendo eventos diferentes diff --git a/es/sandbox/cash-in.mdx b/es/sandbox/cash-in.mdx index f7347c3..0a7f9fd 100644 --- a/es/sandbox/cash-in.mdx +++ b/es/sandbox/cash-in.mdx @@ -19,7 +19,8 @@ curl -X POST https://sandbox.mx.ntxpay.com/api/spei/cash-in \ -d '{ "amountCentavos": 50000, "externalId": "order-001", - "customerName": "Juan Pérez" + "customerName": "Juan Pérez", + "customerEmail": "juan@example.com" }' ``` @@ -69,10 +70,12 @@ Tras ~1 segundo (escenario `success` default), recibes: | Escenario | Webhook | |---|---| -| Default | `CONFIRMED` | -| `error:invalid-clabe` | `FAILED` | -| `error:duplicate-external-id` | `FAILED` | -| `delayed:5s` | `CONFIRMED` tras 5s | +| `success` (default) | `CONFIRMED` | +| `pending_long` | `CONFIRMED` tras ~30s | +| `rejected` | `FAILED` | +| `returned` | `RETURNED` | + +`timeout` y `provider_5xx` fallan en la respuesta HTTP síncrona. `insufficient_funds` y `bad_clabe` **no aplican** a cash-in (devuelven `400 SCENARIO_NOT_APPLICABLE`). Mira [Escenarios](/es/sandbox/scenarios) para la lista completa. diff --git a/es/sandbox/cash-out.mdx b/es/sandbox/cash-out.mdx index 48f00a2..8ab4c27 100644 --- a/es/sandbox/cash-out.mdx +++ b/es/sandbox/cash-out.mdx @@ -25,8 +25,7 @@ curl -X POST https://sandbox.mx.ntxpay.com/api/spei/cash-out \ "destinationClabe": "012180001234567890", "beneficiaryName": "Maria Lopez", "beneficiaryTaxId": "LOPM850101ABC", - "externalId": "payout-001", - "description": "Pago a proveedor" + "concept": "Pago a proveedor" }' ``` @@ -35,10 +34,11 @@ curl -X POST https://sandbox.mx.ntxpay.com/api/spei/cash-out \ ```json { "id": 12346, - "externalId": "payout-001", "status": "PENDING", + "destinationClabe": "012180001234567890", "amountCentavos": 15000, - "clabe": "012180001234567890" + "referenceNumerical": "9876543", + "createdAt": "2026-03-26T10:00:00.000Z" } ``` @@ -71,18 +71,19 @@ Fuerza comportamientos específicos vía el header `X-Sandbox-Scenario`: ```bash curl -X POST https://sandbox.mx.ntxpay.com/api/spei/cash-out \ -H "Authorization: Bearer $TOKEN" \ - -H "X-Sandbox-Scenario: error:insufficient-funds" \ + -H "X-Sandbox-Scenario: 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" | +| Escenario | Cuándo usar | Webhook | +|---|---|---| +| `insufficient_funds` | Validar UX cuando tu cliente intenta pagar sin saldo | `FAILED` (`errorCode: INSUFFICIENT_FUNDS`) | +| `bad_clabe` | Validar manejo de devolución por CLABE inválida | `RETURNED` (`errorCode: INVALID_CLABE`) | +| `rejected` | Validar rechazo genérico del proveedor/red | `FAILED` | +| `returned` | Validar transferencia devuelta por el banco contraparte | `RETURNED` | +| `pending_long` | Validar UX de "transferencia en progreso" (~30s) | `CONFIRMED` | +| `timeout` / `provider_5xx` | Validar fallas upstream síncronas (`504` / `503`) | — (error síncrono) | Mira [Escenarios](/es/sandbox/scenarios) para la lista completa. @@ -93,10 +94,9 @@ 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 INVALID_CLABE_FORMAT` | `400` | CLABE con formato inválido (no exactamente 18 dígitos) | | `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. + Los escenarios `insufficient_funds`, `bad_clabe`, `rejected` y `returned` afectan al **webhook** — la respuesta síncrona es `201 PENDING`. `timeout` y `provider_5xx` fallan de forma síncrona, igual que las validaciones estructurales (monto/formato de CLABE). diff --git a/es/sandbox/scenarios.mdx b/es/sandbox/scenarios.mdx index d061f6a..02848b1 100644 --- a/es/sandbox/scenarios.mdx +++ b/es/sandbox/scenarios.mdx @@ -11,50 +11,47 @@ Agrega el header `X-Sandbox-Scenario: ` a cualquier llamada de cash-i ```bash curl -X POST https://sandbox.mx.ntxpay.com/api/spei/cash-out \ -H "Authorization: Bearer $TOKEN" \ - -H "X-Sandbox-Scenario: error:insufficient-funds" \ + -H "X-Sandbox-Scenario: insufficient_funds" \ -H "Content-Type: application/json" \ -d '{ "amountCentavos": 15000, "destinationClabe": "012180001234567890", - "beneficiaryName": "Maria Lopez", - "externalId": "test-error-001" + "beneficiaryName": "Maria Lopez" }' ``` - 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. + La mayoría de los escenarios controlan el **webhook asíncrono**: la respuesta HTTP es `201 Created` con `status: PENDING`, y el resultado final (`CONFIRMED`, `FAILED` o `RETURNED`) llega en el webhook. Las excepciones son `timeout` y `provider_5xx`, que fallan en la respuesta HTTP **síncrona**. ## Escenarios disponibles -### Escenarios de éxito +Los valores canónicos de escenario son: `success`, `pending_long`, `rejected`, `returned`, `insufficient_funds`, `bad_clabe`, `timeout`, `provider_5xx`. -| Header Value | Comportamiento síncrono | Webhook | -|---|---|---| -| `success` (default) | `201 PENDING` | `CONFIRMED` en ~1s | -| _(sin header)_ | `201 PENDING` | `CONFIRMED` en ~1s | +### Escenarios de resultado asíncrono -### Escenarios de error +Devuelven `201 PENDING` de forma síncrona; el estado final llega vía webhook. -| Header Value | Comportamiento síncrono | Webhook | +| Header Value | Resultado del webhook | Notas | |---|---|---| -| `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` | +| `success` (default) | `CONFIRMED` en ~1s | También se usa cuando no se envía header | +| `pending_long` | `CONFIRMED` tras ~30s | Prueba settlement lento | +| `rejected` | `FAILED` | El proveedor / red SPEI rechazó la transferencia | +| `returned` | `RETURNED` | Aceptada y luego devuelta por el banco contraparte | +| `insufficient_funds` | `FAILED` con `errorCode: INSUFFICIENT_FUNDS` | **Solo cash-out** | +| `bad_clabe` | `RETURNED` con `errorCode: INVALID_CLABE` | **Solo cash-out** — aceptada y luego devuelta | -### Escenarios de atraso +### Escenarios de error síncrono -| 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 | +Fallan en la propia respuesta HTTP — no se envía webhook. + +| Header Value | Respuesta síncrona | +|---|---| +| `timeout` | Timeout upstream (`504`) tras ~16s | +| `provider_5xx` | `503` proveedor no disponible | - El delay máximo permitido es de **120 segundos** — valores mayores se truncan automáticamente. + **Restricciones de cash-in:** `insufficient_funds` y `bad_clabe` no aplican a cash-in (no hay saldo que debitar, y la CLABE de depósito la genera el sistema). Enviar cualquiera de ellos en un cash-in devuelve `400` con código `SCENARIO_NOT_APPLICABLE`. ## Ejemplo: webhook de éxito diff --git a/es/sandbox/webhooks.mdx b/es/sandbox/webhooks.mdx index 609e5fd..03ffa34 100644 --- a/es/sandbox/webhooks.mdx +++ b/es/sandbox/webhooks.mdx @@ -23,7 +23,7 @@ curl -X POST https://sandbox.mx.ntxpay.com/api/webhooks-config \ -H "Content-Type: application/json" \ -d '{ "url": "https://mi-servidor.com/webhooks/ntxpay", - "events": ["cash_in", "cash_out"] + "events": ["cash_in"] }' ``` @@ -33,7 +33,7 @@ curl -X POST https://sandbox.mx.ntxpay.com/api/webhooks-config \ { "id": "wh_550e8400", "url": "https://mi-servidor.com/webhooks/ntxpay", - "events": ["cash_in", "cash_out"], + "events": ["cash_in"], "secret": "whsec_a1b2c3d4...", "createdAt": "2026-03-26T09:00:00.000Z" } @@ -96,7 +96,7 @@ Fuerza que el webhook salga como `FAILED` o con delay: ```bash curl -X POST https://sandbox.mx.ntxpay.com/api/spei/cash-out \ -H "Authorization: Bearer $TOKEN" \ - -H "X-Sandbox-Scenario: delayed:30s" \ + -H "X-Sandbox-Scenario: pending_long" \ -H "Content-Type: application/json" \ -d '{ ... }' ``` diff --git a/pt-br/guides/get-started.mdx b/pt-br/guides/get-started.mdx index c440b53..7a897c4 100644 --- a/pt-br/guides/get-started.mdx +++ b/pt-br/guides/get-started.mdx @@ -36,12 +36,13 @@ A API NTX Pay México permite que sua empresa realize operações de pagamento, ```bash curl -X POST https://sandbox.mx.ntxpay.com/api/spei/cash-in \ -H "Authorization: Bearer $TOKEN" \ - -H "X-Sandbox-Scenario: error:insufficient-funds" \ + -H "X-Sandbox-Scenario: rejected" \ -H "Content-Type: application/json" \ -d '{ "amountCentavos": 50000, "externalId": "order-001", - "customerName": "Juan Perez" + "customerName": "Juan Perez", + "customerEmail": "juan@example.com" }' ``` diff --git a/pt-br/guides/sandbox-testing.mdx b/pt-br/guides/sandbox-testing.mdx index 4d595c1..46ebbd4 100644 --- a/pt-br/guides/sandbox-testing.mdx +++ b/pt-br/guides/sandbox-testing.mdx @@ -27,14 +27,15 @@ O sandbox é o ambiente público da NTX Pay México para integração e testes: ## Como Funciona -O header **não altera a resposta HTTP**. A API sempre responde `201 Created` com `status: "PENDING"`. O cenário afeta exclusivamente o **webhook assíncrono** disparado ~1 segundo depois. +Na maioria dos cenários o header **não altera a resposta HTTP**: a API responde `201 Created` com `status: "PENDING"` e o cenário afeta o **webhook assíncrono** disparado ~1 segundo depois. As exceções são `timeout` e `provider_5xx`, que falham na própria resposta HTTP síncrona. | Request | HTTP Response | Webhook (~1s depois) | |---|---|---| | Sem `X-Sandbox-Scenario` | `201 PENDING` | `CONFIRMED` | | `X-Sandbox-Scenario: success` | `201 PENDING` | `CONFIRMED` | -| `X-Sandbox-Scenario: error:insufficient-funds` | `201 PENDING` | `FAILED` com `errorCode` | -| `X-Sandbox-Scenario: delayed:5s` | `201 PENDING` | `CONFIRMED` com +5s de delay | +| `X-Sandbox-Scenario: insufficient_funds` | `201 PENDING` | `FAILED` com `errorCode` (apenas cash-out) | +| `X-Sandbox-Scenario: pending_long` | `201 PENDING` | `CONFIRMED` após ~30s | +| `X-Sandbox-Scenario: provider_5xx` | `503` (síncrono) | — | ## Como Usar @@ -43,13 +44,12 @@ O header **não altera a resposta HTTP**. A API sempre responde `201 Created` co ```bash cURL curl -X POST https://sandbox.mx.ntxpay.com/api/spei/cash-out \ -H "Authorization: Bearer $TOKEN" \ - -H "X-Sandbox-Scenario: error:insufficient-funds" \ + -H "X-Sandbox-Scenario: insufficient_funds" \ -H "Content-Type: application/json" \ -d '{ "amountCentavos": 15000, "destinationClabe": "012180001234567890", - "beneficiaryName": "Maria Lopez", - "externalId": "test-error-001" + "beneficiaryName": "Maria Lopez" }' ``` @@ -60,12 +60,11 @@ const response = await axios.post( amountCentavos: 15000, destinationClabe: '012180001234567890', beneficiaryName: 'Maria Lopez', - externalId: 'test-error-001', }, { headers: { Authorization: `Bearer ${token}`, - 'X-Sandbox-Scenario': 'error:insufficient-funds', + 'X-Sandbox-Scenario': 'insufficient_funds', }, } ); @@ -78,11 +77,10 @@ response = requests.post( "amountCentavos": 15000, "destinationClabe": "012180001234567890", "beneficiaryName": "Maria Lopez", - "externalId": "test-error-001", }, headers={ "Authorization": f"Bearer {token}", - "X-Sandbox-Scenario": "error:insufficient-funds", + "X-Sandbox-Scenario": "insufficient_funds", }, ) ``` @@ -91,34 +89,28 @@ response = requests.post( ## Cenários Disponíveis -### Cenários de Erro +Valores canônicos: `success`, `pending_long`, `rejected`, `returned`, `insufficient_funds`, `bad_clabe`, `timeout`, `provider_5xx`. -| Header Value | Descrição | Webhook Status | -|---|---|---| -| `error:insufficient-funds` | Conta sem saldo suficiente | `FAILED` | -| `error:invalid-clabe` | CLABE de destino malformada ou inexistente | `FAILED` | -| `error:account-not-found` | Conta destino não localizada na rede SPEI | `FAILED` | -| `error:account-blocked` | Conta destino bloqueada ou encerrada | `FAILED` | -| `error:duplicate-external-id` | `externalId` já usado em outra transação | `FAILED` | -| `error:bank-rejected` | Banco destino rejeitou a transferência (genérico) | `FAILED` | - -### Cenário de Sucesso +### Cenários de resultado assíncrono (`201 PENDING` → webhook) | Header Value | Descrição | Webhook Status | |---|---|---| -| `success` | Força sucesso (mesmo comportamento do default) | `CONFIRMED` | -| _(sem header)_ | Comportamento default do sandbox | `CONFIRMED` | +| `success` _(ou sem header)_ | Comportamento default do sandbox | `CONFIRMED` | +| `pending_long` | Confirma após ~30s (settlement lento) | `CONFIRMED` | +| `rejected` | Provedor / rede SPEI rejeitou | `FAILED` | +| `returned` | Aceita e depois devolvida pelo banco contraparte | `RETURNED` | +| `insufficient_funds` | Conta sem saldo suficiente (**apenas cash-out**) | `FAILED` (`errorCode: INSUFFICIENT_FUNDS`) | +| `bad_clabe` | CLABE inválida (**apenas cash-out**) | `RETURNED` (`errorCode: INVALID_CLABE`) | -### Cenários de Delay +### Cenários de erro síncrono (sem webhook) -| Header Value | Descrição | Webhook Status | +| Header Value | Descrição | HTTP Response | |---|---|---| -| `delayed:5s` | Sucesso após 5 segundos extras | `CONFIRMED` | -| `delayed:30s` | Sucesso após 30 segundos extras | `CONFIRMED` | -| `delayed:60s` | Sucesso após 60 segundos extras | `CONFIRMED` | +| `timeout` | Timeout upstream após ~16s | `504` | +| `provider_5xx` | Provedor indisponível | `503` | - O delay máximo permitido é de **120 segundos** — valores acima são truncados automaticamente. + `insufficient_funds` e `bad_clabe` **não se aplicam a cash-in** — enviá-los em um cash-in retorna `400` com código `SCENARIO_NOT_APPLICABLE`. ## Exemplos de Webhook Recebido @@ -155,7 +147,7 @@ response = requests.post( } ``` -### Webhook de Erro (`error:insufficient-funds`) +### Webhook de Erro (`insufficient_funds`) ```json { @@ -207,7 +199,7 @@ Notas: - Seu sistema recebe o webhook com `status` correspondente ao cenário (`CONFIRMED` ou `FAILED`). - O header controla **apenas o webhook**. A resposta HTTP é sempre `201 Created` com `status: PENDING`, independentemente do cenário. + Na maioria dos cenários o header controla **apenas o webhook**, e a resposta HTTP é `201 Created` com `status: PENDING`. As exceções `timeout` (`504`) e `provider_5xx` (`503`) falham na própria resposta síncrona. ## Boas Práticas diff --git a/pt-br/guides/webhooks/overview.mdx b/pt-br/guides/webhooks/overview.mdx index 6e88c06..46ed92b 100644 --- a/pt-br/guides/webhooks/overview.mdx +++ b/pt-br/guides/webhooks/overview.mdx @@ -23,7 +23,7 @@ Webhooks permitem que o NTX Pay envie notificações HTTPS para o seu servidor s Endpoint: `POST /api/webhooks-config`. Você fornece: - **`url`** — endpoint HTTPS no seu servidor -- **`events`** — array com 1 a 5 eventos +- **`events`** — array com exatamente 1 evento (um webhook assina um evento) - **`secret`** — opcional. Se omitido, o NTX Pay gera um automaticamente e retorna na resposta. Veja o [guia de Setup](/pt-br/guides/webhooks/setup) para o passo a passo. diff --git a/pt-br/guides/webhooks/setup.mdx b/pt-br/guides/webhooks/setup.mdx index c529d31..d3be1d3 100644 --- a/pt-br/guides/webhooks/setup.mdx +++ b/pt-br/guides/webhooks/setup.mdx @@ -22,7 +22,7 @@ curl -X POST https://sandbox.mx.ntxpay.com/api/webhooks-config \ -H "Content-Type: application/json" \ -d '{ "url": "https://meu-servidor.com/webhooks/ntxpay", - "events": ["cash_in", "cash_out"], + "events": ["cash_in"], "secret": "whsec_abc123def456" }' ``` @@ -33,7 +33,7 @@ curl -X POST https://sandbox.mx.ntxpay.com/api/webhooks-config \ { "id": 42, "url": "https://meu-servidor.com/webhooks/ntxpay", - "events": ["cash_in", "cash_out"], + "events": ["cash_in"], "isActive": true, "secret": "whsec_abc123def456" } @@ -50,7 +50,7 @@ curl -X POST https://sandbox.mx.ntxpay.com/api/webhooks-config \ - Lista de 1 a 5 eventos. Valores aceitos: `cash_in`, `cash_out`, `refund_in`, `refund_out`, `internal_transfer`. + Um webhook assina **exatamente UM** evento — o array deve conter um único item. Valores aceitos: `cash_in`, `cash_out`, `refund_in`, `refund_out`, `internal_transfer`. Para receber mais de um tipo de evento, crie um webhook por evento. @@ -71,7 +71,7 @@ curl -X GET https://sandbox.mx.ntxpay.com/api/webhooks-config \ { "id": 42, "url": "https://meu-servidor.com/webhooks/ntxpay", - "events": ["cash_in", "cash_out"], + "events": ["cash_in"], "isActive": true, "createdAt": "2026-05-01T10:30:00.000Z" } @@ -100,9 +100,9 @@ curl -X DELETE https://sandbox.mx.ntxpay.com/api/webhooks-config/42 \ ## Múltiplos Webhooks -Você pode configurar **vários webhooks** simultaneamente, cada um com seu conjunto de eventos. Útil para: +Cada webhook assina exatamente um evento, então configure **um webhook por tipo de evento** que deseja receber (ex.: um para `cash_in`, outro para `cash_out`). Útil para: -- Separar **logs/auditoria** (recebe todos os eventos) de **processamento** (só `cash_in`/`cash_out`) +- Rotear cada tipo de evento para seu próprio endpoint/handler - Múltiplos serviços consumindo eventos diferentes - Ambientes internos distintos (ex.: dev vs homologação interna) diff --git a/pt-br/sandbox/cash-in.mdx b/pt-br/sandbox/cash-in.mdx index c02d88d..209c508 100644 --- a/pt-br/sandbox/cash-in.mdx +++ b/pt-br/sandbox/cash-in.mdx @@ -19,7 +19,8 @@ curl -X POST https://sandbox.mx.ntxpay.com/api/spei/cash-in \ -d '{ "amountCentavos": 50000, "externalId": "order-001", - "customerName": "Juan Pérez" + "customerName": "Juan Pérez", + "customerEmail": "juan@example.com" }' ``` @@ -69,10 +70,12 @@ Após ~1 segundo (cenário `success` padrão), você recebe: | Cenário | Webhook | |---|---| -| Default | `CONFIRMED` | -| `error:invalid-clabe` | `FAILED` | -| `error:duplicate-external-id` | `FAILED` | -| `delayed:5s` | `CONFIRMED` após 5s | +| `success` (default) | `CONFIRMED` | +| `pending_long` | `CONFIRMED` após ~30s | +| `rejected` | `FAILED` | +| `returned` | `RETURNED` | + +`timeout` e `provider_5xx` falham na resposta HTTP síncrona. `insufficient_funds` e `bad_clabe` **não se aplicam** a cash-in (retornam `400 SCENARIO_NOT_APPLICABLE`). Veja [Cenários](/pt-br/sandbox/scenarios) para a lista completa. diff --git a/pt-br/sandbox/cash-out.mdx b/pt-br/sandbox/cash-out.mdx index 3965bb3..6726aef 100644 --- a/pt-br/sandbox/cash-out.mdx +++ b/pt-br/sandbox/cash-out.mdx @@ -25,8 +25,7 @@ curl -X POST https://sandbox.mx.ntxpay.com/api/spei/cash-out \ "destinationClabe": "012180001234567890", "beneficiaryName": "Maria Lopez", "beneficiaryTaxId": "LOPM850101ABC", - "externalId": "payout-001", - "description": "Pagamento de fornecedor" + "concept": "Pagamento de fornecedor" }' ``` @@ -35,10 +34,11 @@ curl -X POST https://sandbox.mx.ntxpay.com/api/spei/cash-out \ ```json { "id": 12346, - "externalId": "payout-001", "status": "PENDING", + "destinationClabe": "012180001234567890", "amountCentavos": 15000, - "clabe": "012180001234567890" + "referenceNumerical": "9876543", + "createdAt": "2026-03-26T10:00:00.000Z" } ``` @@ -71,18 +71,19 @@ Force comportamentos específicos via header `X-Sandbox-Scenario`: ```bash curl -X POST https://sandbox.mx.ntxpay.com/api/spei/cash-out \ -H "Authorization: Bearer $TOKEN" \ - -H "X-Sandbox-Scenario: error:insufficient-funds" \ + -H "X-Sandbox-Scenario: 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" | +| Cenário | Quando usar | Webhook | +|---|---|---| +| `insufficient_funds` | Validar UX quando seu cliente tenta pagar sem saldo | `FAILED` (`errorCode: INSUFFICIENT_FUNDS`) | +| `bad_clabe` | Validar tratamento de devolução por CLABE inválida | `RETURNED` (`errorCode: INVALID_CLABE`) | +| `rejected` | Validar rejeição genérica do provedor/rede | `FAILED` | +| `returned` | Validar transferência estornada pelo banco contraparte | `RETURNED` | +| `pending_long` | Validar UX de "transferência em andamento" (~30s) | `CONFIRMED` | +| `timeout` / `provider_5xx` | Validar falhas upstream síncronas (`504` / `503`) | — (erro síncrono) | Veja [Cenários](/pt-br/sandbox/scenarios) para a lista completa. @@ -93,10 +94,9 @@ 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 INVALID_CLABE_FORMAT` | `400` | CLABE com formato inválido (não exatamente 18 dígitos) | | `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. + Os cenários `insufficient_funds`, `bad_clabe`, `rejected` e `returned` afetam o **webhook** — a resposta síncrona é `201 PENDING`. `timeout` e `provider_5xx` falham síncronos, assim como as validações estruturais (valor/formato de CLABE). diff --git a/pt-br/sandbox/scenarios.mdx b/pt-br/sandbox/scenarios.mdx index 190dac9..a85fcb5 100644 --- a/pt-br/sandbox/scenarios.mdx +++ b/pt-br/sandbox/scenarios.mdx @@ -11,50 +11,47 @@ Adicione o header `X-Sandbox-Scenario: ` a qualquer chamada de cash-in ```bash curl -X POST https://sandbox.mx.ntxpay.com/api/spei/cash-out \ -H "Authorization: Bearer $TOKEN" \ - -H "X-Sandbox-Scenario: error:insufficient-funds" \ + -H "X-Sandbox-Scenario: insufficient_funds" \ -H "Content-Type: application/json" \ -d '{ "amountCentavos": 15000, "destinationClabe": "012180001234567890", - "beneficiaryName": "Maria Lopez", - "externalId": "test-error-001" + "beneficiaryName": "Maria Lopez" }' ``` - 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. + A maioria dos cenários controla o **webhook assíncrono**: a resposta HTTP é `201 Created` com `status: PENDING`, e o resultado final (`CONFIRMED`, `FAILED` ou `RETURNED`) chega no webhook. As exceções são `timeout` e `provider_5xx`, que falham na resposta HTTP **síncrona**. ## Cenários disponíveis -### Cenários de sucesso +Os valores canônicos de cenário são: `success`, `pending_long`, `rejected`, `returned`, `insufficient_funds`, `bad_clabe`, `timeout`, `provider_5xx`. -| Header Value | Comportamento síncrono | Webhook | -|---|---|---| -| `success` (default) | `201 PENDING` | `CONFIRMED` em ~1s | -| _(sem header)_ | `201 PENDING` | `CONFIRMED` em ~1s | +### Cenários de resultado assíncrono -### Cenários de erro +Retornam `201 PENDING` de forma síncrona; o estado final chega via webhook. -| Header Value | Comportamento síncrono | Webhook | +| Header Value | Resultado do webhook | Notas | |---|---|---| -| `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` | +| `success` (default) | `CONFIRMED` em ~1s | Também usado quando nenhum header é enviado | +| `pending_long` | `CONFIRMED` após ~30s | Testa settlement lento | +| `rejected` | `FAILED` | Provedor / rede SPEI rejeitou a transferência | +| `returned` | `RETURNED` | Aceita e depois devolvida pelo banco contraparte | +| `insufficient_funds` | `FAILED` com `errorCode: INSUFFICIENT_FUNDS` | **Apenas cash-out** | +| `bad_clabe` | `RETURNED` com `errorCode: INVALID_CLABE` | **Apenas cash-out** — aceita e depois devolvida | -### Cenários de atraso +### Cenários de erro síncrono -| 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 | +Falham na própria resposta HTTP — nenhum webhook é enviado. + +| Header Value | Resposta síncrona | +|---|---| +| `timeout` | Timeout upstream (`504`) após ~16s | +| `provider_5xx` | `503` provedor indisponível | - O delay máximo permitido é de **120 segundos** — valores acima são truncados automaticamente. + **Restrições de cash-in:** `insufficient_funds` e `bad_clabe` não se aplicam a cash-in (não há saldo a debitar, e a CLABE de depósito é gerada pelo sistema). Enviar qualquer um deles em um cash-in retorna `400` com código `SCENARIO_NOT_APPLICABLE`. ## Exemplo: webhook de sucesso diff --git a/pt-br/sandbox/webhooks.mdx b/pt-br/sandbox/webhooks.mdx index e6072a4..643887c 100644 --- a/pt-br/sandbox/webhooks.mdx +++ b/pt-br/sandbox/webhooks.mdx @@ -23,7 +23,7 @@ curl -X POST https://sandbox.mx.ntxpay.com/api/webhooks-config \ -H "Content-Type: application/json" \ -d '{ "url": "https://meu-servidor.com/webhooks/ntxpay", - "events": ["cash_in", "cash_out"] + "events": ["cash_in"] }' ``` @@ -33,7 +33,7 @@ curl -X POST https://sandbox.mx.ntxpay.com/api/webhooks-config \ { "id": "wh_550e8400", "url": "https://meu-servidor.com/webhooks/ntxpay", - "events": ["cash_in", "cash_out"], + "events": ["cash_in"], "secret": "whsec_a1b2c3d4...", "createdAt": "2026-03-26T09:00:00.000Z" } @@ -96,7 +96,7 @@ Force o webhook sair como `FAILED` ou com delay: ```bash curl -X POST https://sandbox.mx.ntxpay.com/api/spei/cash-out \ -H "Authorization: Bearer $TOKEN" \ - -H "X-Sandbox-Scenario: delayed:30s" \ + -H "X-Sandbox-Scenario: pending_long" \ -H "Content-Type: application/json" \ -d '{ ... }' ``` From 817e165f34f303517fc76f2716a89c10d9e6fd47 Mon Sep 17 00:00:00 2001 From: Adson Rodrigues Date: Mon, 29 Jun 2026 23:58:28 -0300 Subject: [PATCH 2/2] =?UTF-8?q?docs:=20remover=20toda=20men=C3=A7=C3=A3o?= =?UTF-8?q?=20a=20Voluti=20(cliente=20n=C3=A3o=20deve=20conhecer=20o=20pro?= =?UTF-8?q?vedor)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Deleta os guias en/es migrating-from-voluti.mdx (expunham Voluti/magenpay e ainda prometiam aliases já removidos). - Remove os Cards "Migrating from Voluti" dos index en/es. Todos os integradores são clientes NTX; Voluti não aparece na doc pública. Co-Authored-By: Claude Opus 4.8 (1M context) --- en/guides/migrating-from-voluti.mdx | 224 ---------------------------- en/index.mdx | 3 - es/guides/migrating-from-voluti.mdx | 224 ---------------------------- es/index.mdx | 3 - 4 files changed, 454 deletions(-) delete mode 100644 en/guides/migrating-from-voluti.mdx delete mode 100644 es/guides/migrating-from-voluti.mdx diff --git a/en/guides/migrating-from-voluti.mdx b/en/guides/migrating-from-voluti.mdx deleted file mode 100644 index dbeb50c..0000000 --- a/en/guides/migrating-from-voluti.mdx +++ /dev/null @@ -1,224 +0,0 @@ ---- -title: 'Migrating from Voluti SPEI' -description: 'Step-by-step to move your integration from Voluti SPEI (Magen Pay) to NTX Pay México with minimal changes' ---- - -## Overview - -The NTX Pay México API was designed so that customers coming from **Voluti SPEI** (`api.spei.magenpay.io`) can migrate with minimal code changes: - -- **Field aliases** — `payer_name`, `receiverName`, `conciliationId`, `bankCode`, etc. remain accepted -- **amount as string** — `"100.00"` still valid (we convert to centavos internally) -- **Equivalent get-transaction endpoint** — `GET /api/spei/transaction/{externalId}` instead of `GET /api/v1/transaction/{conciliationId}/conciliation` -- **16-digit CLABE** (debit card) accepted on cash-out -- **Auth changes** — Voluti uses HMAC `API_KEY:NONCE:SIGNATURE`; NTX Pay uses **X.509 certificate (mTLS) + OAuth 2.0 → JWT Bearer**. This is the biggest change. - -## 1. Base URL & Auth - -| Item | Voluti | NTX Pay | -|---|---|---| -| Production host | `api.spei.magenpay.io` | Provided at onboarding | -| Sandbox host | — | `sandbox.mx.ntxpay.com` | -| Versioning | `/api/v1` | `/api` | -| Auth header | `Authorization: API_KEY:NONCE:SIGNATURE` | `Authorization: Bearer ` | -| Credential issuance | API key + secret from panel | `POST /api/signup` → `clientId/clientSecret`; then `POST /api/auth/token` with cert X.509 | - -For auth, see [Authentication](/en/guides/authentication). - -## 2. Endpoint Map - -| Voluti | NTX Pay | -|---|---| -| `POST /api/v1/transaction/cashin` | `POST /api/spei/cash-in` | -| `POST /api/v1/transaction/cashout` | `POST /api/spei/cash-out` | -| `GET /api/v1/transaction/balance` | `GET /api/balance` | -| `GET /api/v1/transaction/{conciliationId}/conciliation` | `GET /api/spei/transaction/{externalId}` | -| `POST /webhooks/subscribe` | `POST /api/webhooks-config` | - -## 3. Cash-In Payload - -You can keep the payload **exactly as you send it to Voluti** — we accept the same fields as aliases: - -```bash -# Voluti -curl -X POST https://api.spei.magenpay.io/api/v1/transaction/cashin \ - -H "Authorization: API_KEY:NONCE:SIGNATURE" \ - -d '{ - "amount": "100.00", - "payer_name": "Juan Perez", - "conciliationId": "order_abc_123", - "generateCheckout": true - }' - -# NTX Pay — IDENTICAL payload works -curl -X POST https://sandbox.mx.ntxpay.com/api/spei/cash-in \ - -H "Authorization: Bearer $JWT" \ - -d '{ - "amount": "100.00", - "payer_name": "Juan Perez", - "conciliationId": "order_abc_123" - }' -``` - -Internally we convert to the canonical format: -- `amount: "100.00"` → `amountCentavos: 10000` -- `payer_name` → `customerName` -- `conciliationId` → `externalId` -- `generateCheckout` is ignored — we always return `checkoutUrl` when applicable - -When ready, we recommend migrating to the canonical format: - -```json -{ - "amountCentavos": 10000, - "customerName": "Juan Perez", - "externalId": "order-abc-123" -} -``` - -The response always includes `conciliationId` (mirroring `externalId`) to ease client-side parsing during the transition. - -## 4. Cash-Out Payload - -Same strategy. Accepted aliases: - -| Voluti | NTX Pay canonical | Accepted as alias? | -|---|---|---| -| `amount: "10.00"` (string) | `amountCentavos: 1000` | yes | -| `receiverName` | `beneficiaryName` | yes | -| `receiverClabe` (18 or 16) | `destinationClabe` (18 or 16) | yes | -| `bankCode` | — (not used) | yes, accepted-and-ignored | -| `conciliationId` | `externalId` | yes | - -**16-digit CLABE** (debit card) is accepted in NTX Pay. - -```bash -# Voluti -curl -X POST https://api.spei.magenpay.io/api/v1/transaction/cashout \ - -d '{ - "amount": "100.00", - "receiverName": "Maria Lopez", - "receiverClabe": "012180001234567890", - "bankCode": "002", - "conciliationId": "payout_001" - }' - -# NTX Pay — same payload accepted -curl -X POST https://sandbox.mx.ntxpay.com/api/spei/cash-out \ - -H "Authorization: Bearer $JWT" \ - -d '{ - "amount": "100.00", - "receiverName": "Maria Lopez", - "receiverClabe": "012180001234567890", - "bankCode": "002", - "conciliationId": "payout_001" - }' -``` - -## 5. Transaction Query (polling) - -The dedicated endpoint keeps 1-to-1 semantics with Voluti's `conciliation`: - -```bash -# Voluti -curl -X GET https://api.spei.magenpay.io/api/v1/transaction/order_abc_123/conciliation - -# NTX Pay -curl -X GET https://sandbox.mx.ntxpay.com/api/spei/transaction/order_abc_123 \ - -H "Authorization: Bearer $JWT" -``` - -Returns a single record. The difference is the response schema — see [`GET /api/spei/transaction/{externalId}`](/en/endpoints/spei-transaction). - -## 6. Balance - -```bash -# Voluti -GET /api/v1/transaction/balance - -# NTX Pay -GET /api/balance -``` - -Response structure: - -```json -// NTX Pay -{ - "availableCentavos": 4873490, - "pendingCentavos": 0, - "currency": "MXN" -} -``` - -To convert to the format you used with Voluti (decimal string), divide by 100. - -## 7. Webhooks - -Voluti accepts 3 separate URLs (`default_url`, `cashin_url`, `cashout_url`). In NTX Pay, you create **one webhook per configuration** with the list of events it should receive: - -```bash -# Voluti -POST /webhooks/subscribe -{ - "default_url": "https://my-server.com/webhooks", - "cashin_url": "https://my-server.com/webhooks/cashin", - "cashout_url": "https://my-server.com/webhooks/cashout" -} - -# NTX Pay — one config per URL -POST /api/webhooks-config -{ "url": "https://my-server.com/webhooks/cashin", "events": ["cash_in"] } - -POST /api/webhooks-config -{ "url": "https://my-server.com/webhooks/cashout", "events": ["cash_out", "refund_in", "refund_out"] } -``` - -HMAC validation continues, but with different headers: - -| Voluti | NTX Pay | -|---|---| -| Signature header not publicly documented | `X-NTXPay-Signature: sha256=` | -| — | `X-NTXPay-Timestamp`, `X-NTXPay-Delivery`, `X-NTXPay-Event` | - -Details in [Webhook Implementation](/en/guides/webhooks/implementation). - -## 8. Migration Checklist - - - - Create the account with `POST /api/signup` (use `isSandbox: true` for tests). Store `clientId` + `clientSecret`. - - - Request the X.509 (mTLS) certificate from NTX Pay for the corresponding environment (sandbox / production). - - - Replace the HMAC header assembly with the `POST /api/auth/token` flow (cert + clientId/clientSecret) → cache the JWT for ~10 minutes. - - - From `api.spei.magenpay.io` → your NTX Pay production host (provided at onboarding). Payloads remain the same (aliases accepted). - - - Create 1 or more `POST /api/webhooks-config`. Adjust your handler to validate the NTX Pay HMAC (header `X-NTXPay-Signature`). - - - After stabilizing, swap `amount` for `amountCentavos`, `payer_name` for `customerName`, etc. Not mandatory — aliases remain accepted. - - - -## Known limitations - -- **`generateCheckout`** — ignored. We always return `checkoutUrl` in the response when applicable. -- **`bankCode`** — accepted but ignored. The 18-digit CLABE already carries the bank ISPB. -- **HMAC API_KEY:NONCE:SIGNATURE** — we don't support that scheme. Auth is strictly mTLS + JWT. - -## Next Steps - - - - How to get the JWT (X.509 + OAuth) - - - Schema of the polling endpoint - - diff --git a/en/index.mdx b/en/index.mdx index ec71c09..362aa7d 100644 --- a/en/index.mdx +++ b/en/index.mdx @@ -44,9 +44,6 @@ ISO 8601 UTC: `2026-05-12T14:31:05.000Z`. Signup, token and first transaction - - Payload and endpoint mapping - Cash-in and cash-out via CLABE diff --git a/es/guides/migrating-from-voluti.mdx b/es/guides/migrating-from-voluti.mdx deleted file mode 100644 index c45140f..0000000 --- a/es/guides/migrating-from-voluti.mdx +++ /dev/null @@ -1,224 +0,0 @@ ---- -title: 'Migrando desde Voluti SPEI' -description: 'Paso a paso para mover tu integración desde Voluti SPEI (Magen Pay) a NTX Pay México con mínimas modificaciones' ---- - -## Visión General - -La API NTX Pay México fue diseñada para que clientes provenientes de **Voluti SPEI** (`api.spei.magenpay.io`) puedan migrar con el mínimo de cambios en el código: - -- **Aliases de campo** — `payer_name`, `receiverName`, `conciliationId`, `bankCode`, etc. siguen siendo aceptados -- **amount en string** — `"100.00"` sigue siendo válido (convertimos internamente a centavos) -- **Endpoint de get-transaction equivalente** — `GET /api/spei/transaction/{externalId}` en lugar de `GET /api/v1/transaction/{conciliationId}/conciliation` -- **CLABE de 16 dígitos** (tarjeta de débito) aceptada en cash-out -- **Auth cambia** — Voluti usa HMAC `API_KEY:NONCE:SIGNATURE`; NTX Pay usa **certificado X.509 (mTLS) + OAuth 2.0 → JWT Bearer**. Este es el mayor cambio. - -## 1. URL Base & Auth - -| Item | Voluti | NTX Pay | -|---|---|---| -| Host producción | `api.spei.magenpay.io` | Provista en el onboarding | -| Host sandbox | — | `sandbox.mx.ntxpay.com` | -| Versionado | `/api/v1` | `/api` | -| Auth header | `Authorization: API_KEY:NONCE:SIGNATURE` | `Authorization: Bearer ` | -| Obtención de credenciales | API key + secret entregados desde el panel | `POST /api/signup` → `clientId/clientSecret`; luego `POST /api/auth/token` con cert X.509 | - -Para auth, ver [Autenticación](/es/guides/authentication). - -## 2. Mapa de Endpoints - -| Voluti | NTX Pay | -|---|---| -| `POST /api/v1/transaction/cashin` | `POST /api/spei/cash-in` | -| `POST /api/v1/transaction/cashout` | `POST /api/spei/cash-out` | -| `GET /api/v1/transaction/balance` | `GET /api/balance` | -| `GET /api/v1/transaction/{conciliationId}/conciliation` | `GET /api/spei/transaction/{externalId}` | -| `POST /webhooks/subscribe` | `POST /api/webhooks-config` | - -## 3. Payload Cash-In - -Puedes mantener el payload **exactamente como lo envías a Voluti** — aceptamos los mismos campos como aliases: - -```bash -# Voluti -curl -X POST https://api.spei.magenpay.io/api/v1/transaction/cashin \ - -H "Authorization: API_KEY:NONCE:SIGNATURE" \ - -d '{ - "amount": "100.00", - "payer_name": "Juan Perez", - "conciliationId": "order_abc_123", - "generateCheckout": true - }' - -# NTX Pay — payload IDÉNTICO funciona -curl -X POST https://sandbox.mx.ntxpay.com/api/spei/cash-in \ - -H "Authorization: Bearer $JWT" \ - -d '{ - "amount": "100.00", - "payer_name": "Juan Perez", - "conciliationId": "order_abc_123" - }' -``` - -Internamente convertimos al formato canónico: -- `amount: "100.00"` → `amountCentavos: 10000` -- `payer_name` → `customerName` -- `conciliationId` → `externalId` -- `generateCheckout` es ignorado — siempre retornamos `checkoutUrl` cuando aplica - -Cuando estés listo, recomendamos migrar al formato canónico: - -```json -{ - "amountCentavos": 10000, - "customerName": "Juan Perez", - "externalId": "order-abc-123" -} -``` - -La respuesta siempre incluye `conciliationId` (espejo del `externalId`) para facilitar parsing client-side durante la transición. - -## 4. Payload Cash-Out - -Misma estrategia. Aliases aceptados: - -| Voluti | NTX Pay canónico | ¿Aceptado como alias? | -|---|---|---| -| `amount: "10.00"` (string) | `amountCentavos: 1000` | sí | -| `receiverName` | `beneficiaryName` | sí | -| `receiverClabe` (18 o 16) | `destinationClabe` (18 o 16) | sí | -| `bankCode` | — (no usamos) | sí, aceptado-e-ignorado | -| `conciliationId` | `externalId` | sí | - -CLABE de **16 dígitos** (tarjeta de débito) es aceptada en NTX Pay. - -```bash -# Voluti -curl -X POST https://api.spei.magenpay.io/api/v1/transaction/cashout \ - -d '{ - "amount": "100.00", - "receiverName": "Maria Lopez", - "receiverClabe": "012180001234567890", - "bankCode": "002", - "conciliationId": "payout_001" - }' - -# NTX Pay — mismo payload aceptado -curl -X POST https://sandbox.mx.ntxpay.com/api/spei/cash-out \ - -H "Authorization: Bearer $JWT" \ - -d '{ - "amount": "100.00", - "receiverName": "Maria Lopez", - "receiverClabe": "012180001234567890", - "bankCode": "002", - "conciliationId": "payout_001" - }' -``` - -## 5. Consulta de Transacción (polling) - -El endpoint dedicado mantiene semántica 1-a-1 con el `conciliation` de Voluti: - -```bash -# Voluti -curl -X GET https://api.spei.magenpay.io/api/v1/transaction/order_abc_123/conciliation - -# NTX Pay -curl -X GET https://sandbox.mx.ntxpay.com/api/spei/transaction/order_abc_123 \ - -H "Authorization: Bearer $JWT" -``` - -Retorna un único registro. La diferencia es el esquema del response — ver [`GET /api/spei/transaction/{externalId}`](/es/endpoints/spei-transaction). - -## 6. Saldo - -```bash -# Voluti -GET /api/v1/transaction/balance - -# NTX Pay -GET /api/balance -``` - -Estructura del response: - -```json -// NTX Pay -{ - "availableCentavos": 4873490, - "pendingCentavos": 0, - "currency": "MXN" -} -``` - -Para convertir al formato que usabas con Voluti (string con decimales), divide entre 100. - -## 7. Webhooks - -Voluti acepta 3 URLs separadas (`default_url`, `cashin_url`, `cashout_url`). En NTX Pay, creas **un webhook por configuración** con la lista de eventos que debe recibir: - -```bash -# Voluti -POST /webhooks/subscribe -{ - "default_url": "https://mi-servidor.com/webhooks", - "cashin_url": "https://mi-servidor.com/webhooks/cashin", - "cashout_url": "https://mi-servidor.com/webhooks/cashout" -} - -# NTX Pay — una config por URL -POST /api/webhooks-config -{ "url": "https://mi-servidor.com/webhooks/cashin", "events": ["cash_in"] } - -POST /api/webhooks-config -{ "url": "https://mi-servidor.com/webhooks/cashout", "events": ["cash_out", "refund_in", "refund_out"] } -``` - -La validación HMAC continúa, pero con headers diferentes: - -| Voluti | NTX Pay | -|---|---| -| Header de firma no documentado públicamente | `X-NTXPay-Signature: sha256=` | -| — | `X-NTXPay-Timestamp`, `X-NTXPay-Delivery`, `X-NTXPay-Event` | - -Detalles en [Implementación de Webhook](/es/guides/webhooks/implementation). - -## 8. Checklist de Migración - - - - Crea la cuenta con `POST /api/signup` (usa `isSandbox: true` para tests). Guarda `clientId` + `clientSecret`. - - - Solicita el certificado X.509 (mTLS) a NTX Pay para el ambiente correspondiente (sandbox / producción). - - - Sustituye la construcción del header HMAC por el flujo `POST /api/auth/token` (cert + clientId/clientSecret) → guarda el JWT por ~10 minutos. - - - De `api.spei.magenpay.io` → tu host de producción NTX Pay (provisto en el onboarding). Los payloads siguen iguales (aliases aceptados). - - - Crea 1 o más `POST /api/webhooks-config`. Ajusta tu handler para validar el HMAC NTX Pay (header `X-NTXPay-Signature`). - - - Tras estabilizar, cambia `amount` por `amountCentavos`, `payer_name` por `customerName`, etc. No es obligatorio — los aliases siguen siendo aceptados. - - - -## Limitaciones conocidas - -- **`generateCheckout`** — ignorado. Siempre retornamos `checkoutUrl` en el response cuando aplica. -- **`bankCode`** — aceptado pero ignorado. La CLABE de 18 dígitos ya contiene el ISPB del banco. -- **HMAC API_KEY:NONCE:SIGNATURE** — no soportamos ese esquema. El auth es obligatoriamente mTLS + JWT. - -## Próximos Pasos - - - - Cómo obtener el JWT (X.509 + OAuth) - - - Schema del polling de transacción - - diff --git a/es/index.mdx b/es/index.mdx index 1debbb1..74aa249 100644 --- a/es/index.mdx +++ b/es/index.mdx @@ -44,9 +44,6 @@ ISO 8601 UTC: `2026-05-12T14:31:05.000Z`. Signup, token y primera transacción - - Mapeo de payloads y endpoints - Cash-in y cash-out vía CLABE