Início rápido: o sandbox da API de empresas clientes
Para integrar com o Grip Rails sem dinheiro real: entrada de ARS (créditos nos seus CVU, os números de conta virtual argentinos) e saída de ARS (transferências para CBU, CVU ou alias). O sandbox percorre o mesmo caminho que produção (autorização, reserva no ledger, tarifas, eventos e webhooks), mas com um trilho da COELSA (a câmara de compensação argentina de transferências imediatas) simulado, outro ledger e outro banco de dados. Este guia é o caminho curto; o detalhe está no guia de integração e o contrato, na referência da API.
0. O que entregamos no cadastro (e o que você nos envia)
| O que é | |
|---|---|
| URL base | A que passamos no cadastro; hoje, https://empresas-sandbox.fcmpayments.com. Guarde-a na configuração (por exemplo, FCM_BASE_URL), não no código. |
entity_id |
ent_…, a sua entidade (environment: test). |
| Conta de fundos | acc_…: daqui saem as saídas e aqui são creditadas as entradas. |
| Números de conta | Um ou mais acn_…, cada um com o seu CVU (sintético no sandbox). |
| API key | fcm_test_…. Chega criptografada com a sua chave PGP (ou por um link de uso único). Nunca por chat nem por e-mail sem criptografia. |
keyid de assinatura |
ck_…: o id da SUA chave pública, registrada pelo Grip Rails. |
O que você nos envia antes: só a chave pública de assinatura de requisições (Ed25519 ou P-256, PEM), a sua chave PGP pública para receber a API key e, se quiser webhooks, a sua URL (https na porta 443, TLS 1.2 ou superior, sem redirecionamentos, que responda em menos de 10 s). A chave privada nunca sai do seu sistema:
openssl genpkey -algorithm ed25519 -out fcm-firma-privada.pem
openssl pkey -in fcm-firma-privada.pem -pubout -out fcm-firma-publica.pem # esta é a que você nos envia
1. Autenticação: API key e assinatura de cada requisição
Toda requisição leva as duas coisas:
Authorization: Bearer fcm_test_….- Assinatura HTTP (RFC 9421) feita com a sua chave privada: componentes
@methode@target-uri; com corpo, tambémcontent-type,content-digest(RFC 9530, sobre os bytes exatos que você envia) eidempotency-key. Parâmetros:created(segundos Unix, ±300 s),noncealeatório novo em cada requisição,keyid(ck_…) ealg. A API key não é assinada: uma assinatura que cobreauthorizationé rejeitada (signature_invalid).
@target-uri é a URL base do cadastro + o caminho + a query, exatamente como vão pela rede. Os assinadores prontos para
copiar estão em firma-pedidos.ts, .py e .go.
Todo POST leva Idempotency-Key (16 a 128 caracteres A-Z a-z 0-9 . _ : -; um UUID serve). Limites por entidade:
5 escritas e 25 leituras por segundo; ao ultrapassar, 429 com Retry-After. Os erros típicos no começo:
signature_expired (relógio), signature_replayed (você repetiu um nonce), content_digest_mismatch (você enviou
bytes diferentes dos assinados) e environment_mismatch (uma key fcm_live_ no sandbox).
Exemplo de ponta a ponta: ejemplo-sandbox.ts faz tudo o que está neste guia (Node ≥ 22, sem
dependências; comentários em espanhol). Com firma-pedidos.ts ao lado:
FCM_BASE_URL=https://empresas-sandbox.fcmpayments.com FCM_API_KEY_FILE=./api-key.txt FCM_KEY_ID=ck_… \
FCM_PRIVATE_KEY_FILE=./fcm-firma-privada.pem npx tsx ejemplo-sandbox.ts
2. Valores mágicos do trilho simulado
Valor de uma saída (em centavos; quem decide é a terminação):
| Termina em | Resultado |
|---|---|
…13 |
failed, failure.code: destination_closed (COELSA 7502) |
…70 |
failed, failure.code: rail_expired (COELSA 7570) |
…99 |
fica em submitted até que você o resolva com rail_result (§4) |
| qualquer outra | completed em ~2 s |
Destinos para validar e transferir:
| Destino | Validação | Transferência |
|---|---|---|
alias sbx.valida |
valid (titular com CUIT 27000000006) |
é autorizada |
alias sbx.inactiva |
inactive |
422 destination_inactive |
alias sbx.inexistente |
not_found |
422 destination_not_found |
alias sbx.bloqueada |
blocked |
422 compliance_rejected |
alias sbx.otro.titular |
holder_mismatch se você enviar expected_cuit |
422 destination_holder_mismatch |
| qualquer CBU ou CVU com dígitos verificadores válidos | valid, titular sintético |
é autorizada |
O CUIT (identificação fiscal argentina) 30000000007 sempre dá coincidência no controle de listas de sanções. As tarifas
e os limites do sandbox são de exemplo (você os vê em GET /v1/entity): saída ARS 5 fixos, entrada 1%; limites de
ARS 1.000.000 por transferência, ARS 5.000.000 por dia e ARS 50.000.000 por mês.
3. Entrada de ARS
Simular um crédito (o que em produção é o aviso da COELSA de uma transferência para o seu CVU):
POST /v1/simulations/incoming_transfers
Idempotency-Key: 2f6b1c0e-…
{"account_number_id":"acn_…","amount":1500000,"payer":{"name":"PAGADOR DE PRUEBA"},"concept":"VAR"}
Responde 201 com o crédito posted: amount, net_amount (líquido da tarifa de entrada), pagador, CVU e
ledger_references. Você fica sabendo de duas maneiras, e dá para usar as duas:
- Webhook
transfer.incoming.posted, assinado com JWS (verifique-o comverificar-webhooks.ts,.pyou.goe o JWKS deGET /.well-known/jwks.json). É enxuto: traz o id, não o valor. - Consulta periódica de
GET /v1/events(sem webhooks, ou para conciliar o que o webhook avisou).
Algoritmo de consulta periódica (a cada 2–5 s; uma chamada por consulta e uma por crédito, bem abaixo dos limites):
- Guarde no seu banco de dados o último
sequenceprocessado (no começo,0). - Peça
GET /v1/events?after_sequence=<último>&limit=100sem filtro de tipo: assim a sequência é contígua por entidade (1, 2, 3…). Comtype=há saltos por design e você não consegue detectar lacunas. - Processe cada evento em ordem, de forma idempotente pelo seu
evt_…(e peloitr_…/otr_…do objeto):- se
sequencenão for o último + 1, há uma lacuna: é um erro do Grip Rails, avise-nos e não avance o cursor; transfer.incoming.posted→GET /v1/incoming_transfers/{data.id}para valor, tarifa, pagador e CVU;- um tipo que você não conhece é ignorado (podem aparecer tipos novos sem mudança de versão).
- se
- Guarde o
sequencenovo na mesma transação que o efeito de negócio. - Se
has_morefortrue, peça em seguida a partir denext_after_sequence; senão, espere a próxima rodada.
Os eventos ficam 90 dias. A alternativa GET /v1/incoming_transfers?account_number_id=…&created_after=… também serve,
mas vem do mais novo para o mais antigo e não detecta lacunas.
4. Saída de ARS
-
Valide o destino (recomendado):
POST /v1/counterparty_validationscom{"destination":{"type":"alias","value":"sbx.valida"},"expected_cuit":"27000000006"}→201,result: valide umcval_…que vale por 15 minutos. -
Ordene com uma
Idempotency-KeyNOVA por intenção de pagamento, guardada no seu banco de dados antes de enviar a requisição:HTTPPOST /v1/outgoing_transfers Idempotency-Key: 7c1d9a54-… {"account_id":"acc_…","amount":100000,"currency":"ARS","destination":{"type":"alias","value":"sbx.valida"}, "expected_counterparty_cuit":"27000000006","counterparty_validation_id":"cval_…","external_reference":"pago-123"}Resposta O que significa O que você faz 201reservedAutorizada: o dinheiro (valor + tarifa, total_debit_amount) ficou reservado.completeddireto se o destino for outra conta do Grip Rails.Espera completedoufailed(webhook,/v1/eventsouGET /v1/outgoing_transfers/{id}).202pendingO Grip Rails não conseguiu confirmar a autorização no momento. Pode haver dinheiro reservado. O mesmo que acima. Nunca a repita com outra chave. Termina em reservedou emfailed(canceled_by_fcm).202submitted+status_reason: rail_outcome_unknownUma nova tentativa a encontrou enviada e sem resultado. Espera; o Grip Rails resolve com a COELSA. 422 transfer_declinedA autorização a rejeitou ( insufficient_funds,limit_*,destination_*,compliance_rejected…). Não saiu dinheiro.O error.codediz por quê; otransfer_idfica no estadorejected.5xxou409 idempotency_request_in_progressPode ser repetida ( retryable: true).Repete a mesma requisição com a mesma chave. -
Se a resposta se perder, repita a mesma requisição com a mesma chave: volta a resposta guardada (
Idempotent-Replayed: true) ou o estado atual. A mesma chave com outro corpo é409 idempotency_key_reused. O estado vigente também é consultado comGET /v1/outgoing_transfers?idempotency_key=…ouGET /v1/idempotency_keys/{key}. Use um timeout de cliente de 30 s ou mais nestePOST: a borda espera até 20 s pela autorização. -
Estados:
pending→reserved→submitted→completedoufailed(rejectedsó chega como resposta422, sem evento). Eventos:transfer.outgoing.reserved,transfer.outgoing.completed,transfer.outgoing.failed. Se a COELSA creditar umafaileddepois (liquidação tardia), ela volta asubmittedcomstatus_reason: late_settlement_review(eventotransfer.outgoing.reopened, com o valor já debitado) e o Grip Rails a fecha comocompleted, sem custo (chargestraz umfcm_feede0com o seu motivo). Tratefailedcomo final, exceto por esse evento. No sandbox, você pode provocá-la (passo 6). -
Resolver um
…99manualmente:POST /v1/simulations/outgoing_transfers/{id}/rail_resultcom{"outcome":"completed"}ou{"outcome":"failed","coelsa_code":"7502"}e a suaIdempotency-Key→200com a transferência. Só para umasubmitted;completedcom um código de rejeição é400 parameter_invalid. -
Simular uma liquidação tardia: sobre uma
failedque saiu pela COELSA (por exemplo, uma…13),POST /v1/simulations/outgoing_transfers/{id}/rail_resultcom{"outcome":"completed"}→200com a transferência emsubmitted+status_reason: late_settlement_review, o valor debitado e o eventotransfer.outgoing.reopened. Depois,POST /v1/simulations/outgoing_transfers/{id}/late_settlement_reviewcom{"outcome":"completed"}(duas pessoas do Grip Rails confirmam; no sandbox, dois operadores sintéticos) →202com a transferência ainda reaberta e, alguns segundos depois,transfer.outgoing.completed, sem custo. Como em produção, a partir do primeiro passo as suas saídas ficam desabilitadas (403 entity_suspended). Só no sandbox, alguns segundos depois dessecompletedo Grip Rails volta a habilitá-las sozinho (GET /v1/entityvolta aactive; não se o Grip Rails tiver bloqueado a sua entidade nesse meio-tempo); em produção, continuam desabilitadas até que o Grip Rails as reabilite.
5. Relógio de simulação
Cada entidade do sandbox tem o seu relógio: GET /v1/simulations/clock → {"now":"…","offset_seconds":0}, e
POST /v1/simulations/clock/advance com {"advance_seconds":86400} (até 31 dias por requisição, com
Idempotency-Key) o adianta, nunca o atrasa. Move o dia e o mês de negócio de Buenos Aires (limites diários e
mensais), as novas tentativas de webhooks pendentes e os vencimentos. Por exemplo: chegue ao limite diário
(422 limit_daily_exceeded), adiante um dia e a mesma ordem com outra chave volta a passar.
6. Antes de produção
Em produção mudam a URL base (https://api.fcmpayments.com), a key (fcm_live_…, outra, com a sua chave pública de
produção) e o dinheiro, que é real: a COELSA é irreversível. /v1/simulations/* não existe (404) e os webhooks são
assinados com uma chave de KMS que é publicada no JWKS 7 dias antes de ser usada. O resto é igual: se a sua integração
passar no exemplo de ponta a ponta no sandbox, o contrato é o mesmo.