# SancoPay > SancoPay é uma plataforma brasileira de pagamentos via API focada em Pix. Permite criar cobranças Pix avulsas (cash-in) com código copia e cola, cobranças recorrentes (planos e assinaturas: a cada ciclo a plataforma gera o Pix, envia por e-mail ao cliente e dá baixa automática no pagamento), enviar Pix (cash-out) para chaves de terceiros, um a um ou em lote, pré-aprovar saques com reserva de saldo, consultar saldo, limites e taxa da carteira e receber webhooks assinados. REST + JSON, autenticação Bearer via OAuth (client_id/client_secret), em BRL. Base URL de produção: `https://api.sancopay.com/api/v1`. Sandbox: `https://sandbox.sancopay.com/api/v1`. Todos os caminhos abaixo são relativos a essa base (ex.: `POST /charges` = `POST https://api.sancopay.com/api/v1/charges`), exceto as poucas rotas marcadas como fora do `/v1`. Convenções gerais (valem para todos os endpoints): - Autenticação: `POST /oauth/token` com `client_id` e `client_secret` devolve `{access_token, token_type: "Bearer"}`. Envie `Authorization: Bearer ` em todas as demais chamadas. Resposta 401 = token ausente/inválido/expirado: gere outro. - JSON em `snake_case`, com objetos aninhados (`customer.email`, `pix.key`, `recipient.tax_id`). Envie `Content-Type: application/json`. - Valores em reais como número JSON (`150` ou `150.5`), mínimo 1.00 em cobranças e saques. Datas em ISO 8601 UTC (`"2026-11-09T15:04:05.000000Z"`); vencimentos da recorrência e filtros de data em `YYYY-MM-DD`. - Vocabulário: `reference` = identificador do seu sistema; `metadata` = objeto livre seu (uma lista `[{field, value}]` também é aceita e vira objeto); `tracking_id` = código de rastreio do Pix no processador; `end_to_end_id` = identificador do Pix no Banco Central; `fee` = taxa cobrada de você; `total` = `amount + fee` (o que sai da carteira num saque); `failure_reason` = motivo da falha. - Conta: toda chamada age na conta da credencial que gerou o token; não existe parâmetro para escolher outra conta. - Status sempre em minúsculas. Cobrança: `pending`, `paid`, `failed`, `refunded`, `expired`. Saque: `pending`, `processing`, `completed`, `failed`, `refunded`. Pré-aprovação: `pending`, `approved`, `rejected`, `failed`. Os filtros `status` das listas aceitam esses nomes. - Sucesso de recurso único: o objeto direto, sem envelope. Listas: `{"data": [...], "meta": {"page", "per_page", "total", "last_page"}}`, com `page` e `per_page` (1 a 100, padrão 20). - Erros: `{"message": "...", "code": "...", "errors"?: {"": ["..."]}}`. Decida pelo `code` (estável), não pelo texto. Códigos: `validation_error` (422; `errors` com o caminho, ex. `customer.email`, `items.0.pix.key`), `idempotency_key_required` (422, cash-in sem o header; `errors["Idempotency-Key"]`), `reference_required` (422, saque/pré-aprovação sem `reference` e sem `Idempotency-Key`; `errors.reference`), `duplicate_reference` (409, traz o registro existente em `payout` ou `pre_approval`), `request_in_progress` (409, outro saque da conta em processamento), `limit_exceeded` (422, com `reason` — `daily_count_reached`, `daily_amount_reached`, `daily_amount_left`, `per_cashout_amount_exceeded`, `settlement_hold` — `context` e `errors.amount`), `processor_rejected` (422, recusa síncrona do processador, com `processor_status`; nada é debitado), `recipient_tax_id_required` / `recipient_tax_id_invalid` (422), `key_not_found` (422, validação de chave sem recebedor), `account_not_found` (422, "Nenhuma conta vinculada a este acesso.": o token não tem conta vinculada), `access_denied` (403), `account_closed` (403), `not_found` (404), `processing_error` (400). Mensagens em pt-BR com nomes legíveis dos campos. - Idempotência: `POST /charges` exige o header `Idempotency-Key`; repetir a mesma chave devolve 200 com a cobrança original e o header `Idempotent-Replayed: true`. Saques (`POST /payouts`, cada item de `POST /payouts/batch`) e pré-aprovações (`POST /pre-approvals`) exigem `reference` no corpo ou o header `Idempotency-Key`; `reference` repetida responde 409 `duplicate_reference` com o registro existente. Planos e assinaturas da recorrência: `reference` repetida responde 200 com o registro existente e `Idempotent-Replayed: true` (criação nova = 201). Nunca gere uma chave nova por tentativa. - Tipos de chave Pix (`pix.type`): `cpf`, `cnpj`, `email`, `phone` (`+55DDDNUMERO`), `random` (chave aleatória/EVP). - O nome do processador de pagamentos nunca aparece nas respostas nem nos webhooks. ## Sandbox (ambiente de testes) - [Sandbox](https://sandbox.sancopay.com/api/v1): Mesma API de produção (mesmos caminhos, formatos e mensagens), sem dinheiro real. Credenciais próprias, geradas no painel em Credenciais → Sandbox (a primeira cria a conta de testes com saldo fictício); as de produção não funcionam lá. Cobranças nascem `pending` com copia e cola fictício e só mudam pela simulação. Saques ficam `processing` e viram `completed` sozinhos em poucos segundos, com o webhook `payout.completed`. A `pay_url` da recorrência abre uma página com "Simular pagamento"; os e-mails não são enviados, mas ficam em `emails[]`. Webhooks assinados como em produção. Antes de publicar: trocar a base URL e as credenciais, cadastrar o webhook de produção e remover as chamadas a `/sandbox/*` (em `/api/v1/sandbox/*`, só existem no sandbox). - [POST /sandbox/transactions/{id}/simulate](https://sandbox.sancopay.com/api/v1/sandbox/transactions/{id}/simulate): Só no sandbox. Body `status`: `paid` (credita o valor menos a taxa e envia o webhook `charge.paid`; se for Pix de recorrência, envia também `recurring_charge.paid`), `failed`, `expired` ou `refunded`. A resposta traz a cobrança e, em `webhooks`, o desfecho de cada entrega. - [POST /sandbox/cashouts/{id}/simulate](https://sandbox.sancopay.com/api/v1/sandbox/cashouts/{id}/simulate): Só no sandbox. Body `status`: `completed`, `failed` ou `refunded`. Falha e estorno devolvem o valor e a taxa. - [POST /sandbox/wallet/top-up](https://sandbox.sancopay.com/api/v1/sandbox/wallet/top-up): Só no sandbox. Body `amount` (até 10000 por chamada): saldo fictício. - [POST /sandbox/recurring/subscriptions/{id}/issue-next](https://sandbox.sancopay.com/api/v1/sandbox/recurring/subscriptions/{id}/issue-next): Só no sandbox. Emite agora o próximo ciclo. - [POST /sandbox/recurring/charges/{id}/overdue](https://sandbox.sancopay.com/api/v1/sandbox/recurring/charges/{id}/overdue): Só no sandbox. Vence a cobrança e envia `recurring_charge.overdue`. - [GET /sandbox/webhook-deliveries](https://sandbox.sancopay.com/api/v1/sandbox/webhook-deliveries): Só no sandbox. Últimas entregas de webhook com payload, status HTTP e erro. - Valores para testar erros: saque ou pré-aprovação de `13.13` responde 422 `processor_rejected`; com a validação de chave ativa, chave terminada em `0000` responde 422 `key_not_found`. ## Autenticação (OAuth) - [POST /oauth/token](https://api.sancopay.com/api/v1/oauth/token): Body `client_id` (`spk_live_` + 24 caracteres minúsculos/dígitos em produção; `spk_test_` + 24 no sandbox), `client_secret` (`sps_live_` + 48 em produção; `sps_test_` + 48 no sandbox). O prefixo indica o ambiente: `_live_` = produção, `_test_` = sandbox; a credencial só funciona no ambiente dela. Retorna `{"access_token": "...", "token_type": "Bearer"}`. Guarde o segredo e o token só no servidor. ## Cobranças Pix (cash-in) - [Recurso cobrança](https://api.sancopay.com/api/v1/charges): `{id, tracking_id, reference, status, amount, fee, message, customer: {name, email, tax_id}, pix: {copy_paste}, end_to_end_id, metadata, failure_reason, created_at, updated_at}`. `fee` é null enquanto a taxa ainda não foi calculada; `failure_reason` só com `status: "failed"`; `pix.copy_paste` é o código copia e cola (o mesmo conteúdo do QR Code). - [POST /charges](https://api.sancopay.com/api/v1/charges): Cria uma cobrança. Header obrigatório `Idempotency-Key`. Body: `amount` (obrigatório, mín. 1.00), `reference`, `message` (até 255), `customer` {`name` (até 255), `email`, `tax_id` (CPF/CNPJ, até 50)}, `metadata` (objeto) — todos opcionais exceto `amount`. 201 com a cobrança (`status: "pending"`); repetição idempotente: 200 com a original e `Idempotent-Replayed: true`. Libere o pedido pelo webhook `charge.paid`. - [GET /charges/{id}](https://api.sancopay.com/api/v1/charges/{id}): Retorna a cobrança pelo id (o `id` da criação). 404 `not_found`; 403 `access_denied` se for de outra conta. - [GET /charges](https://api.sancopay.com/api/v1/charges): Lista de cobranças. Query: `status`, `reference` e `tracking_id` (filtros EXATOS), `id`, `date_from`, `date_to` (`YYYY-MM-DD`), `q` (busca livre), `page`, `per_page` (1 a 100, padrão 20). Para achar uma cobrança pela sua `reference` ou pelo `tracking_id`, use `GET /charges?reference=X` (ou `?tracking_id=X`): a resposta é uma lista (vazia quando nada bate, não 404). - [GET /charges/export](https://api.sancopay.com/api/v1/charges/export): Exporta as cobranças em CSV, com os mesmos filtros da lista (a coluna `Referencia` traz a `reference`). ## Saques Pix (cash-out) - [Recurso saque (payout)](https://api.sancopay.com/api/v1/payouts): `{id, tracking_id, reference, status, amount, fee, total, pix: {type, key, masked_key}, recipient: {name, tax_id, bank}, end_to_end_id, pre_approval_id, metadata, failure_reason, created_at, updated_at}`. Falha e devolução retornam o valor ao saldo. - [POST /payouts](https://api.sancopay.com/api/v1/payouts): Envia um Pix. Body: `amount` (obrigatório, mín. 1.00), `reference` (ou header `Idempotency-Key`), `pix` {`type` (obrigatório: `cpf` | `cnpj` | `email` | `phone` | `random`), `key` (obrigatório, até 255)}, `recipient` {`tax_id` (CPF/CNPJ do titular, até 20)}, `verify_key` (bool, padrão true), `metadata`. 201 com o saque. Quando o processador de saque da conta exige o documento, `recipient.tax_id` é obrigatório, exceto com `pix.type` `cpf`/`cnpj` (a própria chave é usada); sem ele: 422 `recipient_tax_id_required`. Erros comuns: 409 `duplicate_reference` (+ `payout`), 409 `request_in_progress`, 422 `reference_required`, 422 `limit_exceeded`, 422 `processor_rejected`. - Validação da chave (módulo DICT, quando ativo para a conta): a primeira tentativa para um destinatário responde 202 `{message, code: "key_verification_required", verification: {id, status: "validating", pix: {type, key}, amount_sent?}}`. Consulte `GET /key-verifications/{id}` (a cada ~3 s) até `status` `completed` ou `failed`; confira `recipient` {`name`, `tax_id`, `bank`} e confirme reenviando o mesmo corpo (mesma `reference`) com `verify_key: false`. Recebedor não encontrado: 422 `key_not_found`. - [GET /key-verifications/{id}](https://api.sancopay.com/api/v1/key-verifications/{id}): Retorna `{id, status (validating | completed | failed), pix: {type, key}, recipient: {name, tax_id, bank}, updated_at}`. 404 `not_found`. - [POST /payouts/batch](https://api.sancopay.com/api/v1/payouts/batch): Body `{items: [ ... ]}` com 1 a 100 itens, cada um com o mesmo corpo do `POST /payouts` (cada item com a sua `reference`). Responde 200 `{items: [{index, http_status, result}]}`, onde `result` é o corpo que o `POST /payouts` daria para aquele item; confira o `http_status` de cada um. - [GET /payouts](https://api.sancopay.com/api/v1/payouts): Lista de saques. Query: `status`, `reference` e `tracking_id` (filtros EXATOS), `name` (parte do nome do destinatário), `key` (parte da chave), `date_from`, `date_to`, `amount_min`, `amount_max`, `page`, `per_page`. Para achar um saque pela `reference` ou pelo `tracking_id`, use `GET /payouts?reference=X` (ou `?tracking_id=X`): a resposta é uma lista. - [GET /payouts/{id}](https://api.sancopay.com/api/v1/payouts/{id}): Retorna o saque pelo id da SancoPay (o `id` do envio, também em `data.id` dos webhooks `payout.*`). 404 `not_found`. ## Pré-aprovação de saque - [Fluxo](https://api.sancopay.com/api/v1/pre-approvals): Cria o saque e reserva o saldo (valor + taxa); o Pix só sai na aprovação. A pré-aprovação é criada mesmo sem saldo (a reserva é tentada de novo na aprovação, que responde 422 se ainda faltar saldo). Se o envio falhar, o valor reservado é liberado. Recurso: `{id, reference, status, amount, fee, total, pix: {type, key}, recipient: {tax_id}, tracking_id, approved_at, created_at}`. - [POST /pre-approvals](https://api.sancopay.com/api/v1/pre-approvals): Body: `amount` (obrigatório), `reference` (ou header `Idempotency-Key`; sem os dois = 422 `reference_required`), `pix` {`type`, `key`} (obrigatório), `recipient` {`tax_id`} (guardado e reenviado na aprovação; obrigatório quando a conta exige, como no `POST /payouts`). 201 com a pré-aprovação; `reference` repetida = 409 `duplicate_reference` com `pre_approval`. - [POST /pre-approvals/{id}/approve](https://api.sancopay.com/api/v1/pre-approvals/{id}/approve): Aprova e envia o Pix. Retorna `{pre_approval, payout}`. - [POST /pre-approvals/{id}/cancel](https://api.sancopay.com/api/v1/pre-approvals/{id}/cancel): Cancela uma pré-aprovação não aprovada e devolve o valor reservado. Retorna a pré-aprovação com `status: "rejected"`. - [GET /pre-approvals](https://api.sancopay.com/api/v1/pre-approvals): Lista. Query `status` (`pending` | `approved` | `rejected` | `failed`), `page`, `per_page`. - [GET /pre-approvals/{id}](https://api.sancopay.com/api/v1/pre-approvals/{id}): Retorna a pré-aprovação pelo id. 404 `not_found`. ## Carteira - [GET /balance](https://api.sancopay.com/api/v1/balance): Retorna `{balance: {available, blocked, current, withdrawable}, limits: {daily: {count, amount, count_limit, amount_limit, remaining_count, remaining_amount} | null, per_payout: número | null}, payout_fee: {model (fixed | percentage | mixed), fixed (reais), percent (%)} | null, max_withdrawable, settlement, updated_at}`. `withdrawable` = disponível menos o que ainda está em liberação (D+N). `max_withdrawable` = maior saque possível agora, já com a taxa. `settlement` (null sem prazo): `settlement_days`, `hold_percent` (100 = tudo retido), `held_amount`, `next_release` {`at`, `amount`}. Saque acima do liberado pelo prazo: 422 `limit_exceeded` com `reason: "settlement_hold"` e `context` {`settlement_days`, `hold_percent`, `held`, `withdrawable`}. ## Cobrança recorrente (assinaturas) - [Visão geral](https://api.sancopay.com/api/v1/recurring/plans): Plano (valor + periodicidade) → assinatura (cliente + plano) → cobrança (um ciclo). Precisa ser liberada para a conta; sem isso as rotas `/recurring/*` respondem 403 com `code: "recurring_billing_disabled"`; verifique com `GET https://api.sancopay.com/api/recurring/access` (fora do `/v1`; `{enabled: true | false}`). No dia do vencimento, a partir das 8h (Brasília), a plataforma gera o Pix e envia ao cliente um e-mail com o copia e cola e a `pay_url` (página de pagamento pública, sem login). A 1ª cobrança sai na criação da assinatura (`issue_first_charge`, padrão true) com vencimento em `start_date`. Lembretes em D+1 e D+3 (e-mails só entre 8h e 21h). No dia seguinte ao vencimento vira `overdue` e continua pagável; Pix expirado é regerado ao abrir a `pay_url`. Pagamento conciliado sozinho → webhook `recurring_charge.paid`. Mensal nos dias 29–31 vence no último dia dos meses curtos. Periodicidades: `weekly`, `biweekly`, `monthly`, `bimonthly`, `quarterly`, `semiannual`, `annual`. Status da assinatura: `active`, `paused`, `cancelled`, `completed`. Status da cobrança: `pending`, `overdue`, `paid`, `cancelled`. IDs são uuid (id de outra conta = 404). Valores em número (mín. 1.00, máx. 999999.99). Mesmo envelope do resto da API (objeto direto; listas `{data, meta}`; erros `{message, code, errors?}`). Leitura exige `transactions.view`; escrita, `payments.pix`. As rotas de personalização do e-mail (`/recurring/email-settings*`) ficam fora do `/v1`, na raiz `https://api.sancopay.com/api`. - [GET /recurring/plans](https://api.sancopay.com/api/v1/recurring/plans): Lista. Query: `is_active`, `q` (nome), `reference`, `page`, `per_page`. Plano: `id`, `name`, `description`, `amount`, `periodicity`, `periodicity_label`, `default_max_cycles`, `is_active`, `reference`, `subscriptions_count`, `active_subscriptions_count`, `created_at`, `updated_at`. - [POST /recurring/plans](https://api.sancopay.com/api/v1/recurring/plans): Body: `name` (obrigatório, até 120), `amount` (obrigatório), `periodicity` (obrigatório), `description` (até 255), `default_max_cycles` (1 a 600; vazio = sem limite), `reference` (até 100; idempotência). 201; `reference` repetida = 200 + `Idempotent-Replayed: true`. - [GET /recurring/plans/{id}](https://api.sancopay.com/api/v1/recurring/plans/{id}): Consulta um plano. - [PATCH /recurring/plans/{id}](https://api.sancopay.com/api/v1/recurring/plans/{id}): Parcial: `name`, `amount`, `description`, `default_max_cycles`, `is_active`. `periodicity` ou `reference` = 422. O novo `amount` vale para as próximas cobranças das assinaturas que seguem o plano. - [DELETE /recurring/plans/{id}](https://api.sancopay.com/api/v1/recurring/plans/{id}): Exclui plano sem assinaturas: 204 sem corpo. Com assinaturas: 409 (desative com `PATCH {"is_active": false}`). - [GET /recurring/subscriptions](https://api.sancopay.com/api/v1/recurring/subscriptions): Lista. Query: `status`, `plan_id`, `reference`, `q` (nome, e-mail ou documento do cliente), `page`, `per_page`. - [POST /recurring/subscriptions](https://api.sancopay.com/api/v1/recurring/subscriptions): Body: `plan_id` (obrigatório, uuid de plano ativo), `customer` {`name` (obrigatório, até 255), `email` (obrigatório), `tax_id` (obrigatório, CPF/CNPJ válido)}, `amount` (vazio = valor do plano), `message` (até 255), `start_date` (`YYYY-MM-DD`, de hoje até 1 ano; padrão hoje), `max_cycles` (1 a 600), `end_date` (≥ `start_date`), `notify_customer` (bool, padrão true), `issue_first_charge` (bool, padrão true), `reference` (até 100). 201 com a assinatura e, no mesmo objeto, `first_charge` (a 1ª cobrança no formato do detalhe, com `pay_url` e `pix`; null com `issue_first_charge: false`). `reference` repetida = 200 + `Idempotent-Replayed: true`, sem emitir outra cobrança. Assinatura: `id`, `reference`, `status`, `status_label`, `plan` {`id`, `name`, `periodicity`, `periodicity_label`, `amount`}, `customer` {`name`, `email`, `tax_id` (só dígitos)}, `amount` (efetivo da próxima cobrança), `amount_override` (null = segue o plano), `message`, `start_date`, `next_due_date`, `upcoming_due_dates` (até 3), `cycles_issued`, `max_cycles`, `end_date`, `notify_customer`, `paused_at`, `cancelled_at`, `completed_at`, `cancel_reason`, `created_at`, `updated_at`. - [GET /recurring/subscriptions/{id}](https://api.sancopay.com/api/v1/recurring/subscriptions/{id}): Consulta uma assinatura. - [PATCH /recurring/subscriptions/{id}](https://api.sancopay.com/api/v1/recurring/subscriptions/{id}): Parcial: `customer` {`name`, `email`, `tax_id`}, `amount` (null = volta ao plano), `message`, `max_cycles`, `end_date`, `notify_customer`. `plan_id`, `start_date`, `periodicity` ou `reference` = 422. Cancelada ou concluída = 409. - [POST /recurring/subscriptions/{id}/pause](https://api.sancopay.com/api/v1/recurring/subscriptions/{id}/pause): Pausa uma `active` (outro status = 409). Sem corpo. - [POST /recurring/subscriptions/{id}/resume](https://api.sancopay.com/api/v1/recurring/subscriptions/{id}/resume): Retoma uma `paused` (outro status = 409), sem cobrança retroativa. Sem corpo. - [POST /recurring/subscriptions/{id}/cancel](https://api.sancopay.com/api/v1/recurring/subscriptions/{id}/cancel): Body opcional: `cancel_open_charges` (bool, padrão true), `reason` (até 255). Já cancelada ou concluída = 409. - [GET /recurring/charges](https://api.sancopay.com/api/v1/recurring/charges): Lista (vencimento decrescente). Query: `status`, `subscription_id`, `due_from`, `due_to`, `q`, `page`, `per_page`. Cobrança: `id`, `subscription_id`, `subscription_reference`, `plan` {`id`, `name`} ou null, `customer` {`name`, `email`}, `cycle_number`, `due_date`, `amount`, `message`, `status`, `status_label`, `pay_url`, `pix` ({`copy_paste`, `transaction_id`, `generated_at`} só com a cobrança em aberto e o Pix válido; senão null), `last_pix_error`, `issued_at`, `overdue_at`, `paid_at`, `paid_transaction_id`, `cancelled_at`, `duplicate_payment`, `duplicate_payment_at`, `created_at`. - [GET /recurring/charges/{id}](https://api.sancopay.com/api/v1/recurring/charges/{id}): Detalhe; confere o pagamento antes de responder. Inclui `transactions[]` ({`id`, `status`, `origin`: `issue` | `reminder` | `public_page` | `manual`, `created_at`}) e `emails[]` ({`kind`: `due` | `reminder_1` | `reminder_2` | `manual_N`, `kind_label`, `status`: `queued` | `sent` | `skipped` | `failed`, `to_email`, `skip_reason`, `failure_reason`, `sent_at`, `created_at`}). - [POST /recurring/charges/{id}/resend](https://api.sancopay.com/api/v1/recurring/charges/{id}/resend): Reenvia o e-mail com o Pix atual (ou novo). Retorna a cobrança no formato do detalhe com `email` {`kind`, `status`}. Paga ou cancelada = 409 `invalid_state`. Máximo 1 a cada 10 minutos e 5 por dia: acima disso 429 `too_many_requests` com `retry_after` (segundos). - [POST /recurring/charges/{id}/cancel](https://api.sancopay.com/api/v1/recurring/charges/{id}/cancel): Cancela cobrança em aberto ou vencida (paga ou cancelada = 409) e envia `recurring_charge.cancelled`. Não cancela a assinatura. - [GET /recurring/email-settings](https://api.sancopay.com/api/recurring/email-settings): `{settings, defaults, placeholders, branding, updated_at}`. Variáveis dos textos: `{{customer_name}}`, `{{company_name}}`, `{{plan_name}}`, `{{description}}` (mensagem da cobrança), `{{amount}}`, `{{due_date}}`, `{{cycle}}`. `branding.logo_source`: `recurring`, `sales_recovery` ou null (marca da plataforma). - [PUT /recurring/email-settings](https://api.sancopay.com/api/recurring/email-settings): Parcial (vazio ou null volta ao padrão): `display_name` (até 120), `due_subject` (até 200), `due_heading` (até 150), `due_message` (até 1000), `reminder_subject`, `reminder_heading`, `reminder_message`, `button_color`, `button_text_color`, `heading_color` (`#RRGGBB`). Texto puro (HTML aparece literal). Nome, cores e logo valem também na `pay_url`. - [POST /recurring/email-settings/preview](https://api.sancopay.com/api/recurring/email-settings/preview): Corpo do PUT + `kind` (`due` | `reminder`); retorna `{subject, sender_name, html}` sem salvar. - [POST /recurring/email-settings/logo](https://api.sancopay.com/api/recurring/email-settings/logo): `multipart/form-data`, campo `logo` (PNG, JPG ou WEBP, até 2 MB, 32 a 4000 px; SVG recusado; regravado como PNG). `DELETE` no mesmo caminho remove. ## Webhooks - [Cadastro](https://api.sancopay.com/api/v1/webhooks): Recurso `{id, url, is_active, created_at, updated_at}`. `GET /webhooks` lista (sem `signing_secret`); `POST /webhooks` body `{url}` (HTTPS público) cria e devolve o `signing_secret` (`whsec_...`) uma única vez; `POST /webhooks/{id}/rotate-secret` gera um novo segredo, devolvido uma única vez (o anterior deixa de valer na hora); `DELETE /webhooks/{id}` remove (204); `POST /webhooks/test` body `{url}` testa se a URL responde antes de cadastrá-la (envia um `webhook.test`) e devolve `{reachable, ok, status, message}`; é a única chamada que sai sem assinatura. - [Envelope](https://api.sancopay.com/api/v1/webhooks): Todos os eventos chegam por POST no mesmo formato `{"event": "", "created_at": "", "data": {...}}`, com o header `X-Webhook-Event-Type` igual ao `event`. Responda 2xx rápido; trate entregas repetidas (use `event` + `data.id`/`data.reference`). - [Eventos de cobrança](https://api.sancopay.com/api/v1/webhooks): `charge.` (`charge.paid`, `charge.failed`, `charge.refunded`, `charge.expired`, `charge.pending`). `data`: `{id, tracking_id, reference, status, amount, fee, currency: "BRL", customer: {name, email, tax_id}, end_to_end_id, metadata, failure_reason, created_at, updated_at}`. - [Eventos de saque](https://api.sancopay.com/api/v1/webhooks): `payout.` (`payout.completed`, `payout.failed`, `payout.refunded`, `payout.processing`). `data`: `{id, tracking_id, reference, status, amount, fee, currency: "BRL", recipient: {name, tax_id}, end_to_end_id, metadata, failure_reason, created_at, updated_at}` (`id` pode ser null; `tracking_id` é o código de rastreio do Pix). - [Eventos de pré-aprovação](https://api.sancopay.com/api/v1/webhooks): `pre_approval.created` | `approved` | `rejected` | `failed`. `data` = recurso da pré-aprovação + extras: `created` traz `amount_blocked` (bool), `balance_before`, `balance_after`; `failed`/`rejected` trazem `failure_reason`; recusa do processador traz `processor_rejected: true` e `processor_status`. - [Eventos da recorrência](https://api.sancopay.com/api/v1/webhooks): `recurring_charge.created` | `paid` | `overdue` | `cancelled` | `duplicate_payment` (este com `data.duplicate_transaction_id`). `data`: `{id, subscription_id, subscription_reference, plan_id, plan_name, cycle_number, due_date, amount, message, status, pay_url, pix: {copy_paste, transaction_id} | null, paid_at, paid_transaction_id, cancelled_at, customer: {name, email}}` (sem CPF/CNPJ). O Pix de cada ciclo também gera `charge.*`, com `reference` `REC-{id da cobrança}-{sequência}` e `metadata` com `recurring_charge_id`, `recurring_subscription_id` e `cycle`; dê baixa no ciclo pelo `recurring_charge.paid` para não contar duas vezes. - [Evento de teste](https://api.sancopay.com/api/v1/webhooks): `webhook.test`, `data` `{message, test: true}`. - [Assinatura HMAC](https://api.sancopay.com/api/v1/webhooks): Toda entrega é assinada. Header `X-Webhook-Signature: t=,v1=`. Para verificar: (1) separe `t` e os valores `v1`; (2) calcule HMAC-SHA256 de `"."` com o `signing_secret` inteiro (incluindo `whsec_`) como chave; (3) compare em tempo constante com cada `v1` e aceite se algum bater; (4) recuse se `t` estiver a mais de 5 minutos do horário atual. Use o corpo BRUTO, antes de qualquer parse do JSON.