Descriptor do provedor

Para: Dev · Adm — Conferido em 2026-10-31.

O descriptor é um documento JSON que todo provedor de liquidação serve em GET /spp/v1/descriptor. Ele é a fonte única de tudo o que varia de um provedor para outro: o que ele sabe fazer, quais campos a credencial tem, para onde ele fala, em que ambiente pode rodar, como o parceiro assina callbacks e quais limites o parceiro impõe. O núcleo do Dracma (executor, api, painel, ops) não guarda lista de provedores nem compara nome de provedor: ele lê o descriptor.

Esta página é a referência campo a campo. O protocolo em si (rotas, erros, HMAC) está no contrato SPP v1; aqui o foco é o documento que você escreve para plugar um provedor.

O que é

  • Um objeto JSON com 21 campos de primeiro nível (17 obrigatórios, 4 opcionais).
  • Servido por GET /spp/v1/descriptor, autenticado por HMAC interno v2 (sem credencial de tenant no corpo, porque o descriptor não depende de tenant).
  • Copiado pelo executor no registro (register) junto com seu hash (descriptor_sha256). Mudar o descriptor muda o hash e dispara as regras da seção Regras de mudança do descriptor.
  • Validado pelo registro contra o schema Descriptor do contrato (additionalProperties: false): campo desconhecido no nível raiz é erro de registro.

Quando usar

  • Você vai escrever um provedor novo (nosso ou de terceiro): o descriptor é a primeira coisa a existir, antes de qualquer rota de negócio.
  • Você vai operar um provedor: o descriptor diz quais hosts liberar no egress, quais IPs de callback conferir e qual política de ambiente vale.
  • Você vai mudar um provedor já plugado (nova capacidade, novo host, novo campo de credencial): as regras de compatibilidade decidem se o provedor continua enabled ou volta a registered.

Como funciona

flowchart LR P[Provedor<br/>GET /spp/v1/descriptor] --> R[Registro do executor<br/>valida e grava sha256] R --> API[api<br/>valida credencial e config<br/>matriz de capacidade] R --> PN[painel<br/>desenha o formulario] R --> OPS[ops<br/>egress, ingress, porta, HMAC] R --> EX[executor<br/>guarda de ambiente, rate limit, timeouts, IPs de callback]

Quem lê o quê:

Consumidor Campos que lê O que deriva
Painel e admin display_name, credential_schema, config_schema, capabilities, links, deprecations Formulário de nova conexão, rótulos, ajuda, campos mascarados, avisos de depreciação
api capabilities, credential_schema, config_schema, limits Validação de credencial e config na criação da conexão, matriz de direção e método (roteamento), GET /currencies de cripto
executor environments, environment_policy, credential_mode, callbacks, idempotency, rate_limits, timeouts, quirks, spp_versions_supported Guarda de ambiente, escolha de base_url, conferência de IP e assinatura de callback, balde de rate limit, prazos, negociação de versão
ops name, egress_hosts, callbacks.source_ips Regra de saída por UID, rota de callback, par HMAC INTERNAL_HMAC_<PROVEDOR>_EXECUTOR, métricas e rótulos

Campos do descriptor

Legenda: Obr. = obrigatório no schema Descriptor. Tipos seguem JSON; MAJOR.MINOR é a expressão ^[0-9]+\.[0-9]+$.

Campo Tipo Obr. Regras
name string ^[a-z0-9_-]{1,64}$ sim Identificador opaco e imutável. Vira o {provider} da rota de callback, o rótulo psp das métricas, o sufixo do segredo HMAC (maiúsculas, - vira _) e o valor gravado no registro.
display_name string, até 80 caracteres sim Rótulo mostrado no painel.
version string (semver) sim Versão do adaptador (não do protocolo).
spp_version MAJOR.MINOR sim A versão do SPP que o provedor fala melhor. Hoje 1.0.
spp_versions_supported array de MAJOR.MINOR, mínimo 1 item sim Todas as versões que ele aceita. O executor escolhe a maior versão comum.
synthetic boolean sim true = dinheiro de mentira (simulador). Recusado em prd e em operação production.
capabilities objeto sim Veja Capacidades.
quirks objeto sim not_found_is_definitive (boolean, default false) e payout_return_hypothetical (informativo).
credential_schema JSON Schema 2020-12 sim Campos da credencial do tenant. Marcadores x-secret, x-label, x-help. Use additionalProperties: false.
credential_mode objeto {field, test_value} não Campo da credencial que declara "credencial de teste". Ausente quando o parceiro não distingue.
config_schema JSON Schema 2020-12 sim Configuração não secreta por conexão (base_url e semelhantes).
environments objeto {sandbox?, live} sim Cada ambiente é {base_url, doc_url?}. Sem sandbox, o parceiro não tem ambiente de teste. live é obrigatório, exceto em synthetic.
environment_policy objeto {live_outside_prd} sim forbidden ou flag. Veja Ambientes e política de ambiente.
egress_hosts array de host:porta, itens únicos sim Única lista de saída que o ops libera. Padrão ^[a-z0-9.-]+:[0-9]{1,5}$. Deve incluir o host de todos os environments.
callbacks objeto {signature, source_ips} sim Assinatura do callback do parceiro (ou null) e IPs de origem.
idempotency objeto sim Como o provedor deduplica. Veja Idempotência.
rate_limits objeto sim Limites observados no parceiro.
timeouts objeto sim Prazos por operação.
limits objeto {min_amount, max_amount, currency} não Faixa de valor do parceiro. Informativo: a decisão de valor é das regras do núcleo.
deprecations array não Recursos em depreciação. Veja Depreciação.
links objeto {docs_url, support} não Documentação e contato de suporte do provedor.

Regras transversais:

  • O objeto raiz não aceita campo extra (additionalProperties: false). Dentro de quirks, limits e links, o leitor do núcleo é tolerante e ignora o que não conhece.
  • O descriptor é servido sem credencial. Nunca coloque segredo nele, nem exemplo de segredo real.
  • name nunca muda. Para "renomear", registre um provedor novo e mova o roteamento.
  • Dinheiro em limits é string inteira na menor unidade ("1000" = R$ 10,00 em BRL).

Capacidades

capabilities é o contrato de "o que este provedor faz". A regra geral do SPP: capacidade true significa que a operação deve funcionar e será coberta pela certificação; capacidade false significa que a rota responde 501 unsupported com effect: none. O núcleo nunca infere capacidade pelo nome do provedor.

Forma completa:

{
  "directions": ["cash_in", "cash_out"],
  "methods": [
    { "method": "pix", "directions": ["cash_in", "cash_out"] }
  ],
  "balance": false,
  "charge_lookup": true,
  "charge_ref_resolution": false,
  "charge_cancel": false,
  "credential_check": true,
  "key_lookup": { "dict": false, "key_types": [] },
  "fee_reporting": "none",
  "refunds": { "payout_returns": false, "charge_refund": false },
  "callbacks": true,
  "event_delivery": "callback"
}
Capacidade Valores Rota SPP ligada Se true, a certificação exige
directions cash_in, cash_out POST /charges (entrada), POST /payouts e POST /payouts/get (saída) Entrada: cobrança idempotente (mesma chave devolve a mesma cobrança, com Spp-Replayed: true; corpo diferente com a mesma chave dá 409 idempotency_conflict). Saída: saque idempotente, falha de rede depois do envio vira upstream_timeout com effect: unknown, e payouts/get responde para toda chave já aceita (ou not_found).
methods lista de {method, directions, assets?} com method em pix ou crypto corpo de POST /charges (campo method) O pedido de cada método declarado é aceito; método não declarado é recusado. assets só é permitido com method: crypto.
balance boolean POST /account/balance Resposta available/blocked/currency; formato irreconhecível dá 502 unrecognized_response, nunca saldo zero. Se false: 501 unsupported.
charge_lookup boolean POST /charges/get Estados pending, paid, expired, observed, unrecognized; em paid, valor e pagador reais (fetch-back). Se false: 501 unsupported (caso atual da MaximaxPay).
charge_ref_resolution boolean POST /charges/resolve-ref Referência do parceiro vira deposit_id; pagamento que não é do Dracma devolve 200 com deposit_id: null, sem erro.
charge_cancel boolean POST /charges/cancel Idempotente (cancelled ou already_terminal). Exigida de provedor cripto (expiração de cobrança).
credential_check boolean POST /credentials/check Faz só leitura inócua no parceiro; credencial recusada pelo parceiro é 200 com valid: false, não erro. Falha de infraestrutura é erro.
key_lookup {dict: boolean, key_types: [..]} POST /keys/lookup Documento completo devolvido e nunca registrado; chave inexistente dá 404 key_not_found com effect: none.
fee_reporting none, on_create, on_query, both campo fee de payouts e payouts/get fee sempre presente na resposta (ok, absent ou unrecognized); tarifa ilegível nunca vira erro de envio; ok só com 0 <= fee <= amount.
refunds.payout_returns boolean estado returned em payouts/get Pagamento devolvido pelo banco do recebedor aparece como returned (valor, motivo, data). Sem esta capacidade o provedor não pode emitir returned.
refunds.charge_refund boolean nenhuma na v1 Deve ser false. O campo reserva lugar para uma operação aditiva (versão MINOR).
callbacks boolean POST /callbacks/verify Verificação de assinatura em tempo constante sobre o corpo cru (body_base64); assinatura inválida dá 422 callback_unauthenticated sem revelar o motivo; resposta ao teste com credencial isca idêntica à de conexão real. Se true, callbacks.signature do descriptor deve estar preenchido.
event_delivery callback, bus, none fora das rotas callback: progresso chega por callback do parceiro. bus: o provedor publica eventos no barramento (caso cripto). none: só consulta.

Combinações incoerentes (a certificação reprova e o registro não deve aceitar):

  • directions contém cash_out mas a rota /payouts responde 501. Vale o inverso para cash_in e /charges.
  • callbacks: true com callbacks.signature: null.
  • methods com assets num método que não seja crypto.
  • charge_cancel: false num provedor cujo método é crypto.
  • refunds.charge_refund: true (a v1 não tem consumidor).

Matriz de capacidade derivada

A matriz de direção por provedor (que antes estava copiada no roteamento do executor, no painel, na api e no cofre de credenciais) passa a ser calculada de directions e methods. Se a sua capacidade está errada no descriptor, o roteamento fica errado em todos os lugares ao mesmo tempo.

credential_schema e config_schema

Os dois são JSON Schema 2020-12. A diferença é de natureza:

credential_schema config_schema
Conteúdo Segredos e identificadores de acesso do tenant no parceiro Configuração não secreta da conexão
Onde vive Cifrada no cofre do executor; vai no corpo de cada chamada (credential) Em claro na conexão; vai no corpo de cada chamada (config)
Aparece em resposta de API? Nunca (campos x-secret são só de escrita) Sim
Exemplos access_token, client_secret, webhook_secret base_url, xpubs, node_urls

Marcadores

Marcador Onde Efeito
x-secret: true em uma propriedade O painel desenha campo mascarado e de escrita única (nunca reexibido); a api e o executor tratam o valor como segredo: não logam, não devolvem, redigem no Debug. Todo campo que dá acesso ou assina algo deve ter.
x-label em uma propriedade Rótulo do campo no painel, em pt-BR.
x-help em uma propriedade Texto de ajuda curto sob o campo (onde achar o valor no painel do parceiro).
additionalProperties: false no objeto raiz Campo desconhecido no pedido vira 422 misconfigured com field indicando o campo.

Os demais campos são JSON Schema comum (type, required, minLength, maxLength, pattern, format, default, enum) e valem como validação.

Exemplo mínimo de uma propriedade:

{
  "access_token": {
    "type": "string",
    "minLength": 1,
    "maxLength": 512,
    "x-secret": true,
    "x-label": "Access token",
    "x-help": "Credenciais de produção do aplicativo, no painel do parceiro."
  }
}

Como painel, api e ops derivam as coisas

Derivação Fonte no descriptor Resultado
Formulário do painel credential_schema.properties, required, x-label, x-help, x-secret, type Um campo por propriedade, com o tipo de controle escolhido pelo type (um boolean vira caixa de seleção); x-secret vira campo mascarado de escrita única.
Validação na api credential_schema e config_schema Corpo de criação/rotação de conexão validado contra o schema antes de gravar; campo fora do schema é recusado.
Validação no provedor os mesmos schemas Toda chamada de negócio é validada de novo (422 misconfigured com field). Não confie só na api.
Egress egress_hosts Regra de saída por UID do provedor: só esses host:porta, mais DNS e telemetria local. Veja a página de segurança do provedor.
Ingress name + registro A rota /callbacks/{provider}/{connection_id} do ingress aceita qualquer provedor registrado e habilitado e encaminha ao executor. Nenhum provedor novo exige editar rota.
IPs de callback callbacks.source_ips[mode] O executor confere o IP de origem antes de chamar callbacks/verify; lista vazia não restringe.
Guarda de ambiente synthetic, environments, environment_policy, credential_mode Tabela da seção a seguir.
base_url da conexão environments[mode].base_url ou valor de config cujo host esteja em egress_hosts Host fora da lista dá 422 misconfigured (anti-SSRF).

Ambientes e política de ambiente

Dois conceitos que não se confundem:

  • modo: sandbox (ambiente de teste do parceiro) ou live (dinheiro real). Quem decide é o executor; o provedor confirma.
  • ambiente de execução: dev, hml ou prd. É fail-closed: ausência de valor é tratada como não-prd.

environments:

{
  "sandbox": { "base_url": "https://sandbox.parceiro.example", "doc_url": "https://docs.parceiro.example/sandbox" },
  "live":    { "base_url": "https://api.parceiro.example" }
}

environment_policy.live_outside_prd aceita dois valores: forbidden (padrão seguro: modo live fora de prd é recusado) e flag (exceção controlada: só com a chave PSP_REAL_OUTSIDE_PRD contendo o name, em hml e em operação production, com a mesma auditoria da ADR-0026).

Tabela da guarda

Provedor Ambiente Modo permitido Regra
synthetic = true dev, hml sandbox Recusado em prd e em operação production (como o simulador hoje).
synthetic = false prd live sandbox em prd é recusado. Credencial de teste é recusada (credential_mode).
synthetic = false, com environments.sandbox dev, hml sandbox Usa só a URL de sandbox; a credencial deve declarar teste (credential_mode).
synthetic = false, sem sandbox dev, hml live (exceção) Exige live_outside_prd = flag e PSP_REAL_OUTSIDE_PRD contendo o name e ambiente hml e operação production. Senão, erro de guarda de ambiente.

Regras adicionais:

  • credential_mode com {"field": "test_credential", "test_value": true}: em sandbox a credencial precisa ter test_credential == true; em live precisa ter false. O provedor pode recusar também por padrão do token (por exemplo o prefixo TEST- do Mercado Pago), respondendo 403 environment_forbidden.
  • Fora de prd, o executor nunca usa environments.live.base_url, salvo a exceção da última linha.
  • Defesa em profundidade: o provedor reaplica a guarda. Ele recusa mode: live com runtime_env diferente de prd, salvo se environment_policy permitir e o executor sinalizar context.live_exception: true. Recusa também mode: sandbox com runtime_env == prd. A resposta é 403 environment_forbidden com effect: none.
  • A chave PSP_REAL_OUTSIDE_PRD=<nome>[,<nome>] vive no seeder da aplicação, só é escrita pelo ops (psp-real) e substitui as duas chaves antigas por provedor.

Detalhes de segurança e de efeito externo estão em Segurança do provedor.

Callbacks

callbacks descreve como o parceiro assina o que manda para o Dracma e de onde ele manda:

{
  "signature": {
    "scheme": "hmac-sha256-hex-prefixed",
    "header": "x-signature",
    "algorithm": "HMAC-SHA256",
    "covers": "raw_body",
    "encoding": "sha256=<hex>"
  },
  "source_ips": { "sandbox": [], "live": ["203.0.113.0/24"] }
}
Campo Tipo Obr. Regras
signature objeto ou null sim null quando o parceiro não assina (e então capabilities.callbacks é false).
signature.scheme string sim Nome livre do esquema, por exemplo hmac-sha256-hex-prefixed ou hmac-sha256-manifest.
signature.header string sim Cabeçalho da assinatura, em minúsculas. É repassado ao provedor em request.headers.
signature.algorithm string sim Por exemplo HMAC-SHA256.
signature.covers string sim raw_body (bytes crus do corpo) ou manifest (texto montado a partir de campos).
signature.encoding string não Formato do valor: sha256=<hex> ou ts=<n>,v1=<hex>.
source_ips objeto {sandbox, live} sim Listas de CIDR por modo. Lista vazia ou ausente não restringe.
paths_extra array não Nunca necessário: a rota de callback é fixa (/callbacks/{provider}/{connection_id}).

Contrato de assinatura no fluxo:

  1. O parceiro chama https://<host>/callbacks/{provider}/{connection_id}.
  2. O ingress encaminha ao executor; o executor acha a conexão, confere o IP contra source_ips[mode], aplica a guarda de ambiente e chama POST /spp/v1/callbacks/verify no provedor, com request (método, path, cabeçalhos do descriptor mais content-type, query, body_base64, received_at).
  3. O provedor verifica a assinatura sobre os bytes de body_base64 (não reserializa JSON), em tempo constante, e valida janela de tempo quando o parceiro assina timestamp.
  4. Resposta 200 {"events": [...]} ou 422 callback_unauthenticated. Ao parceiro o executor responde 200, 401 uniforme (assinatura inválida ou conexão desconhecida) ou 503 (falha transitória, para o parceiro reenviar).
  5. O evento é só uma afirmação (claimed). Nenhum callback move dinheiro sozinho: o executor confirma por consulta (fetch-back) antes de qualquer efeito.

Tipos de evento devolvidos por callbacks/verify:

Event.type Campos Sentido
payout idempotency_key, claimed (estado do saque) Saque mudou de estado
charge deposit_id, claimed (estado da cobrança) Cobrança mudou, com o depósito já identificado
charge_by_ref psp_ref Só a referência do parceiro veio; o executor chama resolve-ref e charges/get
ignored topic? Autêntico, sem efeito

Conexão desconhecida: o executor chama o provedor com uma credencial isca aleatória no formato do credential_schema, para que a resposta externa não revele se a conexão existe. O provedor não pode distinguir isca de credencial real.

Idempotência

{
  "charge_key": "deposit_id",
  "payout_key": "psp_idempotency_key",
  "upstream_dedupe": true,
  "retention_hours": 24
}
Campo Valores Regra
charge_key deposit_id ou charge_id Chave de POST /charges: deposit_id para PIX, charge_id para cripto. Vai em Spp-Idempotency-Key e no corpo; os dois devem coincidir (senão 400 invalid_request).
payout_key psp_idempotency_key ou none Chave de POST /payouts. none quando o provedor não faz saque.
upstream_dedupe boolean true quando o parceiro deduplica pelo identificador que o provedor repassa (forma da MaximaxPay identifier e do Mercado Pago X-Idempotency-Key). false obriga repositório local.
retention_hours inteiro, mínimo 24 Por quanto tempo o resultado de uma chave é guardado para repetição.

Comportamento exigido (S7, §8 do contrato):

  • Mesma chave e mesmo corpo (desconsiderando credential e context.operation_id): mesmo resultado, com Spp-Replayed: true, sem novo efeito no parceiro.
  • Mesma chave com corpo diferente: 409 idempotency_conflict (effect: none).
  • Chamada concorrente com a mesma chave: uma executa; as outras esperam ou recebem 409 idempotency_in_progress (effect: unknown).
  • Repositório local (quando upstream_dedupe é false): chave para resultado, sem segredo e sem PII além do necessário.
  • payouts/get deve responder para qualquer chave já aceita por payouts, ou not_found. É a fonte de verdade para resolver saque ambíguo; o executor nunca reenvia saque às cegas.

Rate limits

{ "requests_per_second": 10, "burst": 20, "per": "connection" }
Campo Tipo Regra
requests_per_second número, mínimo 0 Taxa sustentada que o parceiro tolera.
burst inteiro, mínimo 1 Rajada máxima.
per connection ou provider O balde é por conexão (cada tenant tem o seu limite no parceiro) ou por provedor (limite global da conta do parceiro).

O executor aplica um token bucket no cliente com esses valores. Excesso: espera até o prazo da chamada e depois devolve Unavailable(429) (ou CircuitOpen em create_charge, o que habilita failover seguro de depósito). Declare o limite real do parceiro, não um valor otimista. Separadamente, o próprio provedor pode responder 429 provider_busy com Retry-After quando a sua fila está cheia.

Timeouts

{ "default_ms": 15000, "max_ms": 30000, "payout_create_ms": 30000 }
Campo Regra
default_ms Mínimo 1000. Prazo padrão por operação.
max_ms Máximo 120000. Teto absoluto.
payout_create_ms Opcional. Eleva o prazo de POST /payouts até max_ms.

O executor envia o orçamento da chamada em Spp-Timeout-Ms. O provedor deve limitar a chamada ao parceiro a Spp-Timeout-Ms - 500 ms, para sobrar tempo de responder com o erro certo. O executor mantém um cliente HTTP por provedor, connect de até 1 s, sem proxy e sem redirecionamento. Prazo estourado sem resposta é classificado pelo executor como ambíguo (Timeout), nunca como falha definitiva.

Depreciação

{
  "deprecations": [
    { "feature": "legacy_fee_field", "sunset": "2027-06-30", "replacement": "fee.state" }
  ]
}
Campo Tipo Obr. Regra
feature string sim Nome do recurso depreciado.
sunset data YYYY-MM-DD sim A partir desta data o recurso pode ser removido.
replacement string não O que usar no lugar.
  • O executor suporta o MAJOR corrente e o anterior por pelo menos 12 meses após o anúncio.
  • O provedor pode emitir Deprecation e Sunset (RFC 8594) nas respostas.
  • O registro mostra o aviso ao operador do painel e do ops.
  • A certificação falha para o provedor que ainda usa um recurso cujo sunset já passou.

Regras de mudança do descriptor

O registro compara o descriptor_sha256. Quando muda:

Tipo Exemplos Efeito
Compatível Capacidade nova; novo item em deprecations; novo links; novo valor de version Registrado e auditado; o provedor continua no estado em que está.
Incompatível Capacidade removida; credential_schema mais restrito (campo novo obrigatório, pattern mais estreito, campo removido); egress_hosts alterado; environments alterado O provedor volta a registered automaticamente (deixa de receber trabalho novo até novo enable, que exige saúde, egress/ingress conferidos e certificação do novo hash).
Proibida Mudar name Recusada. name é imutável.

Regras específicas:

  • Capacidade removida com conexão que a usa: recusa da mudança.
  • Qualquer mudança de egress_hosts ou environments exige que o ops reaplique egress antes do enable (o ops confere).
  • O contrato não classifica mudança em quirks, rate_limits, timeouts ou idempotency. Se a mudança altera comportamento financeiro (por exemplo not_found_is_definitive de false para true), trate como incompatível e certifique de novo.
  • Versão MINOR do SPP só adiciona (campo opcional, valor novo de capacidade ou de código de erro, operação opcional). Remover, renomear, tornar obrigatório ou mudar semântica é MAJOR, com outro path (/spp/v2).
  • Todo register, enable, drain, disable, unplug e certify grava quem, quando, motivo, descriptor_sha256 e trace_id.

Descriptors completos

Os quatro descriptors abaixo descrevem o comportamento atual dos adaptadores. A extração dos adaptadores para processos externos não muda comportamento. Valores marcados como referência (rate limits, timeouts) são os que o adaptador usa hoje ou valores conservadores, porque o parceiro não publica limite confirmado.

simulator

Provedor sintético: dinheiro de mentira, só dev e hml. Não tem credencial, não faz chamada externa e não recebe callback. Os roteiros de saque (pay, fail, timeout, pay_and_timeout, manual) existem no adaptador e o centavo 77 aciona o modo manual.

{
  "name": "simulator",
  "display_name": "Simulador (sandbox)",
  "version": "1.0.0",
  "spp_version": "1.0",
  "spp_versions_supported": ["1.0"],
  "synthetic": true,
  "capabilities": {
    "directions": ["cash_in", "cash_out"],
    "methods": [
      { "method": "pix", "directions": ["cash_in", "cash_out"] }
    ],
    "balance": false,
    "charge_lookup": true,
    "charge_ref_resolution": false,
    "charge_cancel": false,
    "credential_check": false,
    "key_lookup": { "dict": true, "key_types": ["cpf", "cnpj", "email", "phone", "evp"] },
    "fee_reporting": "none",
    "refunds": { "payout_returns": true, "charge_refund": false },
    "callbacks": false,
    "event_delivery": "none"
  },
  "quirks": {
    "not_found_is_definitive": true,
    "payout_return_hypothetical": false
  },
  "credential_schema": {
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "type": "object",
    "additionalProperties": false,
    "properties": {}
  },
  "config_schema": {
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "type": "object",
    "additionalProperties": false,
    "properties": {}
  },
  "environments": {
    "sandbox": { "base_url": "http://127.0.0.1:7101/spp/v1" }
  },
  "environment_policy": { "live_outside_prd": "forbidden" },
  "egress_hosts": [],
  "callbacks": {
    "signature": null,
    "source_ips": { "sandbox": [], "live": [] }
  },
  "idempotency": {
    "charge_key": "deposit_id",
    "payout_key": "psp_idempotency_key",
    "upstream_dedupe": false,
    "retention_hours": 24
  },
  "rate_limits": { "requests_per_second": 1000, "burst": 1000, "per": "provider" },
  "timeouts": { "default_ms": 15000, "max_ms": 30000 },
  "links": { "support": "dev@dracma.invalid" }
}

Notas do simulador:

  • egress_hosts vazio: o usuário do simulador não tem saída de rede nenhuma (só loopback e telemetria local).
  • synthetic: true dispensa live em environments.
  • payout_returns: true porque o adaptador sabe responder returned (RETURNED, motivo sim_returned). O motivo de falha (sim_rejected) respeita [a-z0-9_]{1,40}.
  • Consulta de cobrança inexistente responde recusa de negócio (404), que o protocolo traduz para upstream_rejected com upstream_status: 404.

maximaxpay

PIX de entrada e saída. Credencial client_id, client_secret e webhook_secret. Callback assinado por HMAC-SHA256 sobre o corpo cru, no cabeçalho x-signature com o valor sha256=<hex>. Não há ambiente de teste do parceiro: por isso só existe live, e environment_policy é flag (exceção controlada de hml, ADR-0026).

{
  "name": "maximaxpay",
  "display_name": "MaximaxPay (PIX)",
  "version": "1.0.0",
  "spp_version": "1.0",
  "spp_versions_supported": ["1.0"],
  "synthetic": false,
  "capabilities": {
    "directions": ["cash_in", "cash_out"],
    "methods": [
      { "method": "pix", "directions": ["cash_in", "cash_out"] }
    ],
    "balance": true,
    "charge_lookup": false,
    "charge_ref_resolution": false,
    "charge_cancel": false,
    "credential_check": true,
    "key_lookup": { "dict": false, "key_types": [] },
    "fee_reporting": "both",
    "refunds": { "payout_returns": false, "charge_refund": false },
    "callbacks": true,
    "event_delivery": "callback"
  },
  "quirks": {
    "not_found_is_definitive": false,
    "payout_return_hypothetical": true
  },
  "credential_schema": {
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "type": "object",
    "additionalProperties": false,
    "required": ["client_id", "client_secret", "webhook_secret"],
    "properties": {
      "client_id": {
        "type": "string",
        "minLength": 1,
        "maxLength": 256,
        "x-label": "Client ID",
        "x-help": "Identificador da integração no painel da MaximaxPay. Vai no cabeçalho x-client-id."
      },
      "client_secret": {
        "type": "string",
        "minLength": 1,
        "maxLength": 512,
        "x-secret": true,
        "x-label": "Client secret",
        "x-help": "Segredo da integração. Vai no cabeçalho x-client-secret e nunca é exibido de novo."
      },
      "webhook_secret": {
        "type": "string",
        "minLength": 1,
        "maxLength": 512,
        "x-secret": true,
        "x-label": "Segredo do webhook",
        "x-help": "Segredo usado pela MaximaxPay para assinar os callbacks (cabeçalho x-signature)."
      }
    }
  },
  "config_schema": {
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "type": "object",
    "additionalProperties": false,
    "required": ["base_url"],
    "properties": {
      "base_url": {
        "type": "string",
        "format": "uri",
        "pattern": "^https://",
        "default": "https://api.maximaxpay.com",
        "x-label": "URL base da API",
        "x-help": "O host precisa estar em egress_hosts do descriptor."
      }
    }
  },
  "environments": {
    "live": { "base_url": "https://api.maximaxpay.com" }
  },
  "environment_policy": { "live_outside_prd": "flag" },
  "egress_hosts": ["api.maximaxpay.com:443"],
  "callbacks": {
    "signature": {
      "scheme": "hmac-sha256-hex-prefixed",
      "header": "x-signature",
      "algorithm": "HMAC-SHA256",
      "covers": "raw_body",
      "encoding": "sha256=<hex>"
    },
    "source_ips": { "sandbox": [], "live": [] }
  },
  "idempotency": {
    "charge_key": "deposit_id",
    "payout_key": "psp_idempotency_key",
    "upstream_dedupe": true,
    "retention_hours": 24
  },
  "rate_limits": { "requests_per_second": 5, "burst": 10, "per": "connection" },
  "timeouts": { "default_ms": 15000, "max_ms": 30000, "payout_create_ms": 30000 },
  "links": { "support": "suporte@parceiro.example" }
}

Notas da MaximaxPay (todas vêm do adaptador):

  • Idempotência pelo parceiro: o campo identifier da MaximaxPay recebe deposit_id na cobrança e psp_idempotency_key no saque. Por isso upstream_dedupe é true. Na cobrança, psp_ref é o próprio deposit_id.
  • Sem consulta de cobrança (lacuna L1): charge_lookup: false, então POST /charges/get e POST /charges/resolve-ref respondem 501 unsupported.
  • Consulta de saque por GET /gateway/transfers?clientIdentifier=<chave>. Chave desconhecida dá not_found, não definitivo (not_found_is_definitive: false): silêncio do parceiro não prova que o saque não saiu.
  • Devolução (returned): o formato do parceiro ainda é hipotético. O adaptador recusa o estado e devolve unrecognized, por isso payout_returns: false e payout_return_hypothetical: true.
  • Tarifa: lida na criação e na consulta; ilegível vira unrecognized, nunca erro de envio.
  • Saldo: GET /gateway/account/balance, formato desconhecido vira unrecognized_response, nunca zero. credential_check é uma leitura de saldo.
  • Nome do titular: owner.name é normalizado pelo adaptador e owner.ip (IPv4 do jogador) é obrigatório no saque.
  • Callback: a assinatura é obrigatória no Dracma (a documentação do parceiro a chama de opcional). O corpo é interpretado como cobrança quando identifier é UUID, e como saque no restante.
  • IPs de callback: não publicados pelo parceiro; as listas ficam vazias (não restringem) até que ele informe.
  • base_url por conexão: obrigatório (HTTPS fora de dev). O padrão é https://api.maximaxpay.com.

mercadopago

Só entrada PIX (cash_in). O Mercado Pago não tem API pública de saque PIX para chave de terceiro: POST /payouts e POST /payouts/get respondem 501 unsupported sem chamar o parceiro. Credencial access_token, webhook_secret e test_credential. O callback é assinado sobre um manifesto, não sobre o corpo.

{
  "name": "mercadopago",
  "display_name": "Mercado Pago (PIX)",
  "version": "1.0.0",
  "spp_version": "1.0",
  "spp_versions_supported": ["1.0"],
  "synthetic": false,
  "capabilities": {
    "directions": ["cash_in"],
    "methods": [
      { "method": "pix", "directions": ["cash_in"] }
    ],
    "balance": false,
    "charge_lookup": true,
    "charge_ref_resolution": true,
    "charge_cancel": false,
    "credential_check": false,
    "key_lookup": { "dict": false, "key_types": [] },
    "fee_reporting": "none",
    "refunds": { "payout_returns": false, "charge_refund": false },
    "callbacks": true,
    "event_delivery": "callback"
  },
  "quirks": {
    "not_found_is_definitive": false,
    "payout_return_hypothetical": false
  },
  "credential_schema": {
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "type": "object",
    "additionalProperties": false,
    "required": ["access_token", "webhook_secret"],
    "properties": {
      "access_token": {
        "type": "string",
        "minLength": 1,
        "maxLength": 512,
        "x-secret": true,
        "x-label": "Access token",
        "x-help": "Token de acesso do aplicativo (Authorization: Bearer). Em teste, o prefixo costuma ser TEST-."
      },
      "webhook_secret": {
        "type": "string",
        "minLength": 1,
        "maxLength": 512,
        "x-secret": true,
        "x-label": "Assinatura secreta do webhook",
        "x-help": "Painel Suas integrações, seção Webhooks. Usada para validar o cabeçalho x-signature."
      },
      "test_credential": {
        "type": "boolean",
        "default": false,
        "x-label": "Credencial de teste",
        "x-help": "Marque somente se for uma credencial de teste do Mercado Pago. Credencial de teste é recusada em produção."
      }
    }
  },
  "credential_mode": { "field": "test_credential", "test_value": true },
  "config_schema": {
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "type": "object",
    "additionalProperties": false,
    "properties": {
      "base_url": {
        "type": "string",
        "format": "uri",
        "pattern": "^https://",
        "default": "https://api.mercadopago.com",
        "x-label": "URL base da API",
        "x-help": "Opcional. Omitido, usa https://api.mercadopago.com."
      }
    }
  },
  "environments": {
    "sandbox": { "base_url": "https://api.mercadopago.com" },
    "live": { "base_url": "https://api.mercadopago.com" }
  },
  "environment_policy": { "live_outside_prd": "forbidden" },
  "egress_hosts": ["api.mercadopago.com:443"],
  "callbacks": {
    "signature": {
      "scheme": "hmac-sha256-manifest",
      "header": "x-signature",
      "algorithm": "HMAC-SHA256",
      "covers": "manifest",
      "encoding": "ts=<n>,v1=<hex>"
    },
    "source_ips": { "sandbox": [], "live": [] }
  },
  "idempotency": {
    "charge_key": "deposit_id",
    "payout_key": "none",
    "upstream_dedupe": true,
    "retention_hours": 24
  },
  "rate_limits": { "requests_per_second": 10, "burst": 20, "per": "connection" },
  "timeouts": { "default_ms": 15000, "max_ms": 30000 },
  "links": { "support": "suporte@parceiro.example" }
}

Notas do Mercado Pago (todas vêm do adaptador):

  • Mesmo host nos dois modos: o Mercado Pago distingue teste de produção pela credencial, não pelo host. Por isso sandbox e live apontam para o mesmo base_url, e é o credential_mode que separa os dois: sandbox exige test_credential: true, live exige false. O adaptador ainda confere live_mode em cada resposta do parceiro e recusa divergência como misconfigured. Em live, também recusa token com prefixo TEST-.
  • Manifesto assinado: o cabeçalho é x-signature: ts=<ts>,v1=<hex> e v1 é o HMAC-SHA256 do texto id:<data.id em minúsculas>;request-id:<x-request-id>;ts:<ts>; (a parte request-id é omitida quando o cabeçalho não veio). O data.id vem da query (data.id=...). A assinatura não cobre o corpo: o corpo só é conferido contra o data.id assinado e todo efeito vem da consulta.
  • Cabeçalhos repassados em callbacks/verify: x-signature e x-request-id; a query chega em request.query.
  • Janela anti-replay: o adaptador aceita ts em segundos ou milissegundos e rejeita fora de 600 s.
  • Eventos: callback com tópico diferente de payment vira ignored; o restante vira charge_by_ref com o data.id, e o executor confirma com resolve-ref e charges/get.
  • Consulta: charge_lookup usa GET /v1/payments/search?external_reference=<deposit_id>; charge_ref_resolution usa GET /v1/payments/{id}. A fonte de verdade da cobrança paga é essa consulta, nunca o callback.
  • Idempotência: X-Idempotency-Key determinística derivada do deposit_id (32 hex de SHA-256 de um texto fixo mais o id). Repetir a criação do mesmo depósito devolve o mesmo pagamento.
  • Pagador: o e-mail do pagador é obrigatório; sem ele o adaptador recusa com upstream_rejected sem chamar o parceiro.
  • Validade: a expiração pedida é presa à faixa aceita pelo parceiro (31 minutos a 30 dias menos 1 hora) e o instante efetivo é devolvido em expires_at.
  • Sem saque: payout_key: none e directions só com cash_in.

chain

Provedor de recebimento em cripto (dracma_chain_rs). Cobrança por endereço único derivado de xpub (watch-only), progresso por fatos de consulta e por eventos no barramento. Nunca afirma paid: quem decide é a api. Não há saque na v1.

{
  "name": "chain",
  "display_name": "Cripto (on-chain)",
  "version": "1.0.0",
  "spp_version": "1.0",
  "spp_versions_supported": ["1.0"],
  "synthetic": false,
  "capabilities": {
    "directions": ["cash_in"],
    "methods": [
      {
        "method": "crypto",
        "directions": ["cash_in"],
        "assets": [
          { "code": "BTC",  "network": "bitcoin",  "exponent": 8,  "confirmations_required": 3,  "testnet": false, "payment_uri_scheme": "bitcoin" },
          { "code": "LTC",  "network": "litecoin", "exponent": 8,  "confirmations_required": 6,  "testnet": false, "payment_uri_scheme": "litecoin" },
          { "code": "ETH",  "network": "ethereum", "exponent": 18, "confirmations_required": 12, "testnet": false, "payment_uri_scheme": "ethereum" },
          { "code": "USDT", "network": "ethereum", "exponent": 6,  "confirmations_required": 12, "testnet": false, "payment_uri_scheme": "ethereum" },
          { "code": "USDC", "network": "ethereum", "exponent": 6,  "confirmations_required": 12, "testnet": false, "payment_uri_scheme": "ethereum" },
          { "code": "TRX",  "network": "tron",     "exponent": 6,  "confirmations_required": 20, "testnet": false, "payment_uri_scheme": "tron" },
          { "code": "USDT", "network": "tron",     "exponent": 6,  "confirmations_required": 20, "testnet": false, "payment_uri_scheme": "tron" }
        ]
      }
    ],
    "balance": false,
    "charge_lookup": true,
    "charge_ref_resolution": false,
    "charge_cancel": true,
    "credential_check": false,
    "key_lookup": { "dict": false, "key_types": [] },
    "fee_reporting": "none",
    "refunds": { "payout_returns": false, "charge_refund": false },
    "callbacks": false,
    "event_delivery": "bus"
  },
  "quirks": {
    "not_found_is_definitive": false,
    "payout_return_hypothetical": false
  },
  "credential_schema": {
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "type": "object",
    "additionalProperties": false,
    "properties": {}
  },
  "config_schema": {
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "type": "object",
    "additionalProperties": false,
    "properties": {
      "xpubs": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "bitcoin":  { "type": "string", "x-label": "xpub Bitcoin" },
          "litecoin": { "type": "string", "x-label": "xpub Litecoin" },
          "ethereum": { "type": "string", "x-label": "xpub Ethereum" },
          "tron":     { "type": "string", "x-label": "xpub Tron" }
        },
        "x-label": "Chaves públicas estendidas (watch-only)",
        "x-help": "Uma por rede. A chave privada nunca existe neste provedor. Mainnet fora de prd é recusada no boot."
      },
      "node_urls": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "bitcoin":  { "type": "string", "format": "uri" },
          "litecoin": { "type": "string", "format": "uri" },
          "ethereum": { "type": "string", "format": "uri" },
          "tron":     { "type": "string", "format": "uri" }
        },
        "x-label": "URLs dos nós por rede",
        "x-help": "Cada host precisa estar em egress_hosts."
      }
    }
  },
  "environments": {
    "sandbox": { "base_url": "https://node-testnet.exemplo.example" },
    "live":    { "base_url": "https://node-mainnet.exemplo.example" }
  },
  "environment_policy": { "live_outside_prd": "forbidden" },
  "egress_hosts": [
    "node-testnet.exemplo.example:443",
    "node-mainnet.exemplo.example:443"
  ],
  "callbacks": {
    "signature": null,
    "source_ips": { "sandbox": [], "live": [] }
  },
  "idempotency": {
    "charge_key": "charge_id",
    "payout_key": "none",
    "upstream_dedupe": false,
    "retention_hours": 168
  },
  "rate_limits": { "requests_per_second": 20, "burst": 40, "per": "provider" },
  "timeouts": { "default_ms": 15000, "max_ms": 30000 },
  "links": { "support": "dev@dracma.invalid" }
}

Notas do provedor cripto (contrato SPP v1, seção 15):

  • code e network separados: no descriptor o ativo é {code: "USDT", network: "tron"}; nas rotas públicas da api ele é escrito USDT.TRON. As capacidades do provedor mandam: a api monta GET /currencies e valida asset_not_supported a partir daqui. Provedor ausente significa capacidades vazias e cripto indisponível, sem derrubar o boot da api.
  • Confirmações e expoentes acima seguem o catálogo piso do contrato de cobrança em cripto; o provedor pode declarar outro valor, e é o descriptor que vale.
  • Credencial vazia: não há segredo de tenant. A xpub e os nós vão em config (não são segredo de assinatura; a chave privada nunca existe aqui).
  • Sem callback: callbacks: false, assinatura null. O progresso chega por charges/get (state: observed com facts: received, confirmations, confirmations_required, txids, reorged) e por eventos dracma.<env>.chain.*.v1 via outbox (event_delivery: bus). Barramento fora do ar: o outbox acumula e retoma, e a api reconcilia por charges/get.
  • Ambiente: environments.sandbox é testnet; live é mainnet, só em prd. Chave estendida de mainnet fora de prd faz o provedor recusar subir (environment_mismatch). Fora de prd as instruções saem com testnet: true.
  • Idempotência: por charge_id; repetir devolve o mesmo endereço (derivation_index não é reutilizado). A retenção de 168 horas é do repositório local do provedor (upstream_dedupe: false).
  • Saúde: GET /health traz checks por rede (head_height, lag_seconds, nodes_agree); degraded quando nó atrás ou divergente.
  • Saque fora da v1: directions só com cash_in; POST /payouts responde 501.

Os hosts de nó do exemplo são fictícios

Os domínios exemplo.example do descriptor chain são ilustrativos. O ops preenche egress_hosts com os nós reais de cada ambiente. A chave estendida (xpub) revela todos os endereços da operação: trate como dado sensível mesmo estando em config, restrinja o acesso ao arquivo do usuário Linux do provedor e não a escreva em log.

Erros comuns

Sintoma Causa provável Correção
Registro recusa com incompatible_spp_version spp_versions_supported sem versão que o executor fale Inclua a versão comum (1.0)
422 misconfigured com field ao criar conexão Campo fora do credential_schema ou config_schema (por additionalProperties: false) Corrija o payload ou o schema
Cobrança funciona em dev e falha em hml com environment_forbidden Provedor synthetic: false sem environments.sandbox e sem a chave PSP_REAL_OUTSIDE_PRD Declare sandbox ou use a exceção auditada de hml
Callback do parceiro volta 401 para tudo callbacks.signature com header em maiúsculas, ou segredo do webhook trocado Declare o cabeçalho em minúsculas e confira webhook_secret
Egress bloqueado em hml Host de algum environments não está em egress_hosts Inclua todos os hosts e reaplique o egress com o ops
Provedor volta a registered sozinho Mudança incompatível de descriptor Reaplique egress/ingress, certifique o novo hash e faça enable
Capacidade true e a rota responde 501 Descriptor mente Corrija o descriptor ou implemente a rota; a certificação falha nos dois casos
Erro de formato no egress_hosts Faltou a porta Use sempre host:porta
name rejeitado Maiúsculas, espaço ou mais de 64 caracteres Use ^[a-z0-9_-]{1,64}$

Próximos passos

  • Segurança do provedor: seguranca.md (isolamento, egress por UID, credencial por chamada, modelo de ameaça e checklist).
  • Depois do descriptor, implemente as rotas do protocolo e passe a suíte de conformidade antes de pedir enable em hml ou prd.