Integração com a API de empresas clientes
Este guia acompanha o contrato da API. Os exemplos foram escritos para poder ser copiados para um cliente real, mas devem sempre ser testados primeiro no sandbox.
Para começar pelo sandbox: o início rápido e o exemplo de ponta a ponta
ejemplo-sandbox.ts.
1. Escolha o ambiente
| Produção | Sandbox | |
|---|---|---|
| URL base | https://api.fcmpayments.com |
https://empresas-sandbox.fcmpayments.com |
| API key | fcm_live_… |
fcm_test_… |
| Dinheiro | Real; a COELSA (a câmara de compensação argentina de transferências imediatas) é irreversível | Simulado; ledger e trilho separados |
| Simulações | Não disponíveis (404) | /v1/simulations/* |
Uma chave de um ambiente nunca serve no outro.
2. Início rápido
Antes de fazer a primeira requisição, o Grip Rails precisa ter cadastrado:
- o
entity_idda empresa; - uma API key para o ambiente escolhido; e
- a chave pública de assinatura de requisições (
ed25519ouecdsa-p256-sha256), com o seukeyid(ck_…).
A chave privada fica sempre no sistema da empresa. Não é enviada ao Grip Rails, não é guardada em variáveis de ambiente compartilhadas e não é incluída em logs.
Para um POST:
- Serialize o corpo uma única vez e conserve esses bytes exatos.
- Calcule
Content-Digest: sha-256=:<base64>:sobre esses bytes. - Gere uma
Idempotency-Keynova para cada intenção. Se a resposta se perder, repita a mesma requisição com a mesma chave. - Monte a assinatura HTTP RFC 9421 cobrindo
@methode@target-uri; com corpo, acrescentecontent-type,content-digesteidempotency-key. Se você enviarFCM-Version, cubra tambémfcm-version. Não cubraauthorization: a API key vai na requisição, mas não na assinatura (o Grip Rails respondesignature_invalid). Assim a assinatura de cada ordem pode ser verificada de novo com a sua chave pública sem conhecer a sua API key. - Use um
noncealeatório novo em cada requisição (16 bytes em base64url, por exemplo) ecreatedcom a hora atual: o Grip Rails rejeita umnoncerepetido (signature_replayed) e uma assinatura com mais de 5 minutos. - Envie
Authorization,Signature-Input,Signature,Content-DigesteIdempotency-Keyjunto com o corpo.
Componentes extras. Além dos obrigatórios, a borda aceita que a assinatura cubra date, content-length,
@authority, @path e @query (lista fechada; qualquer outro componente é rejeitado). @authority é o host em
minúsculas, @path o caminho (/ se estiver vazio) e @query a query com o seu ? (só ? se não houver). Como
parâmetros da assinatura são aceitos created, expires, nonce, keyid, alg e tag, com qualquer rótulo de
assinatura.
Os trechos completos estão em firma-pedidos.ts, firma-pedidos.py e
firma-pedidos.go: os três geram o nonce sozinhos e só aceitam um fixo para reproduzir um vetor de teste.
Todos implementam Ed25519; o adaptador de chave P-256 usa a mesma base de assinatura e declara
alg="ecdsa-p256-sha256".
# Uma requisição GET não leva Idempotency-Key nem Content-Digest.
curl --fail-with-body \
-H 'Authorization: Bearer fcm_test_…' \
-H 'Signature-Input: …' \
-H 'Signature: …' \
'https://empresas-sandbox.fcmpayments.com/v1/events?after_sequence=0'
Todas as escritas (POST) exigem Idempotency-Key. A mesma chave com o mesmo método, caminho e corpo devolve a
resposta original e Idempotent-Replayed: true; com outro corpo, devolve 409 idempotency_key_reused.
3. Transferências de saída: 201, 202 e 422
POST /v1/outgoing_transfers responde:
-
201com a transferênciareserved(oucompletedse o destino é uma conta do Grip Rails): a autorização aprovou e o dinheiro ficou reservado. -
422 transfer_declinedcom a transferênciarejected: a autorização a rejeitou e não saiu dinheiro. -
202com a transferênciapending:Se o Grip Rails não conseguir confirmar o resultado no momento:
202com a transferência empending, ou emsubmittedcomstatus_reason: rail_outcome_unknownse uma nova tentativa a encontrar já enviada e sem resultado. A ordem existe e está vinculada à sua Idempotency-Key: não a repita com outra chave. Umapendingtermina emreserved(eventotransfer.outgoing.reserved) ou emfailedcomfailure.code: canceled_by_fcm(eventotransfer.outgoing.failed).Estado O que significa Reserva ISO 20022 pendingO Grip Rails recebeu a ordem, mas ainda não consegue confirmar a autorização. Pode haver dinheiro reservado. Não repita com outra Idempotency-Key. Pode haver uma pendente PDNGUma resposta
202é guardada e repetida como qualquer2xx. O estado vigente é consultado comGET /v1/outgoing_transfers/{id}ou chega por webhook. Emauthorization.decision,pendingquer dizer que a autorização ainda não terminou (decided_até a hora em que ficou pendente).
Créditos internos (fcm_book). Uma transferência para um CVU (número de conta virtual argentino) de outra empresa
cliente do Grip Rails é liquidada no ledger do Grip Rails, sem passar pela COELSA. A empresa que recebe vê um crédito
recebido com rail.name: fcm_book; o seu rail.operation_id é o otr_… de origem (a chave de idempotência do
lançamento), em vez do id da COELSA. Chega com o seu evento transfer.incoming.posted e o seu webhook, como qualquer
crédito.
Liquidação tardia. Se a COELSA credita uma transferência que já informamos a você como failed (o aviso chegou
tarde ou se perdeu), o dinheiro saiu: a transferência volta a submitted com status_reason: late_settlement_review, o
valor é debitado com um lançamento compensatório e chega o evento transfer.outgoing.reopened (com as suas
referências). O Grip Rails desabilita as suas saídas (continuam desabilitadas, mesmo depois de completed, até que o
Grip Rails volte a habilitá-las) e, quando duas pessoas do Grip Rails confirmam a liquidação, a fecha como completed
(evento transfer.outgoing.completed) sem custo: o Grip Rails não cobra tarifa por uma transferência reaberta,
porque a reabertura não é culpa sua. charges informa isso com um fcm_fee de 0 e o seu motivo, e
total_debit_amount é o valor. Trate failed como final, exceto por esse evento. No sandbox é possível provocá-la de
ponta a ponta (§5); lá, diferentemente de produção, as suas saídas voltam a ser habilitadas sozinhas depois do
fechamento.
4. Eventos e webhooks
O webhook é um aviso enxuto. O corpo contém o tipo, a sequência e a referência ao objeto; para o estado vigente, faça o
GET correspondente. Processe-o assim:
- Conserve os bytes brutos do corpo. Verifique
FCM-Signatureantes de interpretar o JSON. - Leia
kidealgdo cabeçalho JWS e busque exatamente essa chave emGET /.well-known/jwks.json(em cache; diante de umkiddesconhecido, atualize no máximo uma vez por minuto). O algoritmo do cabeçalho precisa coincidir com o publicado para essekid. - Valide a assinatura,
iatdentro dos últimos cinco minutos eexpainda futuro (até 30 s de margem de relógio),iss(https://api.fcmpayments.com) eaudigual ao seuentity_id. - Recalcule
body_sha256sobre os bytes brutos (base64url sem=) e compare com o claim. Valide tambémevtcontrabody.ide o cabeçalhoFCM-Event-Id, ejticontraFCM-Delivery-Id. - Rejeite um
jtique você já processou: o anti-replay é obrigatório. Guarde cadajtipor pelo menos dez minutos, num armazenamento compartilhado se você tiver várias instâncias, e persista a deduplicação junto com o efeito de negócio. Cada tentativa de entrega tem o seu própriojti(uma nova tentativa traz outrojtie umFCM-Delivery-Attemptmaior). - Deduplique também por
event.ide ordene porsequence. Se você vir o 41 e depois o 43, recupere o 42 comGET /v1/events?after_sequence=41. - Responda
2xxem menos de dez segundos (contando desde que o Grip Rails começa a resolver o seu host até terminar de ler a sua resposta) e processe em segundo plano. Um2xxé o único sucesso; um redirecionamento é uma falha.
Ignore os claims e os tipos de evento que você não conhece: podem aparecer sem aviso de versão.
A política é pelo menos uma vez: 25 tentativas (a primeira e 24 novas tentativas), com variação aleatória de ±20%:
30s, 1m, 2m, 5m, 10m, 15m, 30m, 1h, 2h, 3h, 4h, 5h e depois a cada 8h (cerca de 4,7 dias). Os eventos ficam 90 dias
em /v1/events.
A sua URL. O Grip Rails a registra no cadastro e ela é alterada mediante pedido verificado. Precisa ser https://
na porta 443, com TLS 1.2 ou superior, sem usuário, senha nem fragmento, e resolver só para endereços públicos: o Grip
Rails não entrega para redes privadas nem reservadas, nem para faixas da COELSA ou do Grip Rails. Se o Grip Rails
mudar a sua URL, as entregas pendentes deixam de ir para a antiga; o que ficou no caminho é reenviado para a nova a
pedido, ou você o recupera com GET /v1/events.
A verificação pronta para copiar está em verificar-webhooks.ts, verificar-webhooks.py e
verificar-webhooks.go. As três suportam EdDSA Ed25519 e ES256 P-256, exigem uma proteção anti-replay
(trazem uma em memória, segura com threads ou goroutines, só para começar) e toleram claims novos. O Grip Rails as testa
contra os vetores da suíte de conformidade.
5. Sandbox determinístico
O sandbox usa contas, ledger, eventos e webhooks próprios, e percorre o mesmo caminho que produção: um crédito simulado é lançado, cobra tarifa, emite o seu evento e dispara o webhook. Os valores mágicos permitem testar os caminhos sem tocar em dinheiro real:
| Entrada | Resultado |
|---|---|
Valor termina em …13 |
failed, destination_closed (COELSA 7502) |
Valor termina em …70 |
failed, rail_expired (COELSA 7570) |
Valor termina em …99 |
Fica em submitted até injetar o resultado |
| Qualquer outro valor | Completa-se em aproximadamente 2 s |
Liquidação tardia, em dois passos:
- Sobre uma transferência
failedque saiu pela COELSA (por exemplo, uma…13),POST /v1/simulations/outgoing_transfers/{id}/rail_resultcom{"outcome":"completed"}simula que a COELSA a creditou depois: responde200com a transferência emsubmittedestatus_reason: late_settlement_review, o valor é debitado com o lançamento compensatório e chegatransfer.outgoing.reopened. POST /v1/simulations/outgoing_transfers/{id}/late_settlement_reviewcom{"outcome":"completed"}simula que duas pessoas do Grip Rails confirmam a liquidação (no sandbox, dois operadores sintéticos, pelo mesmo caminho que em produção): responde202com a transferência ainda reaberta e, alguns segundos depois, chegatransfer.outgoing.completed, sem custo.
Como em produção, depois do primeiro passo as suas transferências de saída ficam desabilitadas
(403 entity_suspended). Só no sandbox, alguns segundos depois do transfer.outgoing.completed do segundo passo o
Grip Rails volta a habilitá-las sozinho (GET /v1/entity volta a active; não se o Grip Rails tiver bloqueado a sua
entidade nesse meio-tempo) e você pode continuar testando. Em produção, não: continuam desabilitadas até que o Grip
Rails as reabilite.
Alias (os nomes legíveis das contas argentinas) para validar contrapartes:
| Alias | Validação | Transferência |
|---|---|---|
sbx.valida |
valid |
é autorizada |
sbx.inactiva |
inactive |
destination_inactive |
sbx.inexistente |
not_found |
destination_not_found |
sbx.bloqueada |
blocked |
compliance_rejected |
sbx.otro.titular |
holder_mismatch se você enviar expected_cuit |
destination_holder_mismatch |
Também são aceitos CBU (números de conta bancária argentinos) e CVU com dígitos verificadores válidos, com titular sintético.
Um fluxo mínimo de sandbox é:
POST /v1/simulations/incoming_transfers
-> crédito lançado, evento transfer.incoming.posted e webhook
POST /v1/simulations/outgoing_transfers/{id}/rail_result
{"outcome":"completed"|"failed", "coelsa_code":"7502"}
(para uma transferência submitted; completed com um código de rejeição é 400 parameter_invalid;
sobre uma failed que saiu pela COELSA, só completed: a liquidação tardia)
POST /v1/simulations/outgoing_transfers/{id}/late_settlement_review
{"outcome":"completed"}
(só para uma reaberta: 202, o Grip Rails a fecha como completed sem custo e, só no sandbox,
depois volta a habilitar as suas transferências de saída)
GET /v1/simulations/clock
POST /v1/simulations/clock/advance {"advance_seconds":86400}
O relógio só avança e afeta limites diários/mensais, novas tentativas de webhooks e vencimentos. Os POST de simulação
também levam assinatura e Idempotency-Key.
6. Suíte de conformidade
Antes de passar para produção, peça ao Grip Rails a suíte de conformidade: casos de teste em JSON para rodar contra a sua integração, com chaves de exemplo. Essas chaves são públicas: nunca as registre em nenhum ambiente.
Os erros são entregues como {error:{type,code,message,display_message,param,request_id,retryable,doc_url,transfer_id}}.
Para um erro retryable: true, repita a mesma requisição com a mesma Idempotency-Key; nunca invente uma chave nova
para uma transferência cuja resposta se perdeu.