Contatos
Contatos representam clientes (customer) e fornecedores (supplier) da sua conta. Para cobranças individuais (billing_issue_type=individual), o contact_id do cliente é obrigatório.
Veja conventions.md para paginação, autenticação, erros, rate limit e regras de isolamento entre contas.
Modelo
| Campo | Tipo | Observações |
|---|---|---|
id | integer | |
contact_type | string | "customer" ou "supplier". O tipo dependent existe no Sacador mas não é acessível pela API |
code | string | null | Identificador livre, até 20 chars |
active | boolean | Default true |
email | string | null | Formato de email válido, até 160 chars |
legal_entity | boolean | true = pessoa jurídica (CNPJ); false = pessoa física (CPF) |
full_name | string | Pessoa física: obrigatório, até 255 chars, aceita apenas letras e espaços (sem números, sem pontuação). Pessoa jurídica: opcional |
company_name | string | Obrigatório se legal_entity=true, até 255 chars. Aceita letras, números, espaço e / & , . : - |
trademark | string | Nome fantasia. Obrigatório se legal_entity=true, até 255 chars, mesmos caracteres de company_name |
birth_date | date | null | Opcional. Se enviada, a pessoa precisa ter ≥ 18 anos |
cpf | string | Pessoa física: obrigatório, exatamente 11 dígitos, CPF válido. Único por (conta, contact_type) |
cnpj | string | Pessoa jurídica: obrigatório, 14 caracteres (números ou letras A-Z; CNPJ alfanumérico válido a partir de jul/2026). Único por (conta, contact_type) |
business_phone_area, business_phone_number | string | DDD 2 dígitos; número 8 ou 9 dígitos |
cell_phone_area, cell_phone_number | string | Idem |
address_postal_code | string | CEP com 9 caracteres (formato 00000-000) |
address_street | string | 5..255 chars |
address_number | string | 1..10 chars |
address_complement | string | até 255 chars |
address_district | string | 3..255 chars |
address_city | string | 2..255 chars |
address_state | string | Sigla de UF (SP, RJ, …). Normalizada para maiúsculas no servidor |
observation | string | null | Texto livre |
external_id | string | null | Identificador do contato no seu sistema, até 60 chars. Único por conta. Espaços nas pontas são removidos; enviar em branco apaga o valor. É por ele que créditos e a baixa de fiado por repasse localizam o contato. Ver Fiado e créditos do PDV |
on_account_limit | decimal | null | Limite de fiado do cliente no PDV, em reais. null tem significado diferente conforme o contato tenha ou não external_id, ver Limite de fiado |
store_credit_balance | decimal | Somente leitura. Saldo atual de créditos do cliente. Ignorado se enviado no POST/PATCH |
on_account_balance | decimal | Somente leitura. Fiado em aberto: soma do due_amount das vendas concluídas do contato que ainda têm saldo a pagar. "0.0" quando não há |
on_account_allowed | boolean | Somente leitura. Se o contato pode comprar fiado no PDV conforme as tags. false só quando a conta restringe o fiado por tag e o contato não tem nenhuma dessas tags. Ver Fiado restrito por tag |
description | string | Derivado: company_name se PJ, full_name se PF. Não enviar — calculado pelo Sacador |
document | string | Derivado: cnpj se PJ, cpf se PF |
tags | array | Tags aplicadas ao contato, cada uma {id, name, color}. Definidas via tag_ids — ver tags |
custom_fields | array | Campos personalizados preenchidos, cada um {key, label, value}. Somente leitura — ver Campos personalizados |
created_at, updated_at | ISO-8601 |
Regras de PF vs PJ
O campo legal_entity controla quais validações são aplicadas:
legal_entity=false(PF): exigecpf, exigefull_nameno formato regex;cnpjecompany_namesão opcionais/ignorados na validação cruzada.legal_entity=true(PJ): exigecnpj,company_nameetrademark.
Em ambos os casos, endereço completo é obrigatório (CEP, rua, número, bairro, cidade, UF) e o telefone (qualquer um dos pares) é opcional. A exceção é o contato com external_id, criado por integração: ele pode vir sem nenhum campo de endereço. Se vier com parte do endereço, precisa vir completo, e um endereço já gravado não pode ser apagado, porque os boletos emitidos para o contato copiam o endereço dele. Sem endereço, o contato compra no PDV, mas não recebe boleto até ter um endereço completo. email é opcional, mas sem ele o contato não recebe lembrete de cobrança por e-mail.
Fiado e créditos do PDV
Estes campos ligam o contato ao PDV (ponto de venda) do Sacador:
| Campo | Quem grava | Para que serve |
|---|---|---|
external_id | Você | Vincula o contato ao cadastro do seu sistema |
on_account_limit | Você (ou a loja, pela tela) | Teto do fiado do cliente |
store_credit_balance | O Sacador | Saldo de créditos, movimentado por /v1/store_credits e pelas vendas |
on_account_balance | O Sacador | Fiado em aberto, movimentado pelas vendas, pelos recebimentos e pela baixa por repasse |
tag_ids | Você (ou a loja, pela tela) | Libera o fiado quando a conta o restringe por tag, ver Fiado restrito por tag |
on_account_allowed | O Sacador | Resultado da regra de tags para o contato |
Para vincular um cliente que a loja já tinha cadastrado, localize-o pelo documento com GET/v1/contacts?document= e grave o external_id com um PATCH. Depois disso, use GET/v1/contacts?external_id= para encontrá-lo.
Limite de fiado
No PDV, uma venda só pode deixar no fiado o que couber no limite: o fiado em aberto do cliente (on_account_balance) somado ao fiado da nova venda não pode passar de on_account_limit. Se passar, o PDV recusa a finalização e mostra quanto ainda está disponível.
on_account_limit | Contato sem external_id | Contato com external_id |
|---|---|---|
Valor (ex.: "300.0") | Limite de R$ 300,00 | Limite de R$ 300,00 |
"0.0" | Não compra fiado | Não compra fiado |
null | Sem limite | Vale como zero: não compra fiado |
A diferença no null é proposital: um contato vinculado ao seu sistema só compra fiado depois que você envia um limite para ele.
Fiado restrito por tag
A loja pode marcar tags como "Permite fiado e uso de créditos" (campo allows_on_account da tag). Enquanto nenhuma tag da conta estiver marcada, nada muda: vale só o limite acima. Quando existe ao menos uma tag marcada, o fiado fica restrito:
- Só o contato que tem ao menos uma tag marcada pode finalizar uma venda com valor em aberto. Para os demais,
on_account_allowedéfalsee o PDV recusa a finalização. - A tag libera, o limite define o teto. Para quem tem a tag, a tabela de Limite de fiado continua valendo como está: um contato com
external_ide sem limite enviado continua sem comprar fiado. Um contato semexternal_ide sem limite, mas com a tag, compra fiado sem teto. - A regra barra só fiado novo. Fiado já em aberto, recebimentos, baixa por repasse e devoluções não mudam.
- Créditos seguem a mesma regra. Sem uma tag marcada, o contato não paga com créditos no PDV nem recebe devolução em créditos. O saldo dele fica intocado e volta a valer quando a tag voltar. A inclusão de crédito pela API continua aceita.
Para liberar ou retirar o fiado e os créditos de um contato, envie as tags dele com tag_ids no POST/PATCH. Lembre que tag_ids substitui o conjunto inteiro de tags do contato.
Formato de envio. on_account_limit é lido em reais, nunca em centavos. Aceita número JSON (250, 99.9) ou string com ponto decimal ("250", "99.90"), com no máximo duas casas decimais e nunca negativo. null ou "" apagam o limite. Vírgula ("1.000,00") é recusada com 422:
{
"status": 422,
"errors": ["on_account_limit deve ser um número com até duas casas decimais, separadas por ponto (ex.: 5.00)."]
}
Campos personalizados
Cada conta pode definir campos próprios de cadastro de contato (matrícula, placa do veículo, número
do apartamento, turma, o que fizer sentido para o negócio). Eles são criados na tela
Configurações → Campos personalizados e aparecem em todas as respostas dos endpoints de
contatos, no array custom_fields:
"custom_fields": [
{ "key": "matricula", "label": "Matrícula", "value": "2026-0117", "read_only": false },
{ "key": "turma", "label": "Turma", "value": "3º ano B", "read_only": false },
{ "key": "protocolo", "label": "Protocolo", "value": "2026-0042", "read_only": true }
]
| Campo | Tipo | Observações |
|---|---|---|
key | string | Identificador estável do campo ([a-z][a-z0-9_]*, até 40 chars). Use este valor para casar com o seu sistema — o label pode ser renomeado a qualquer momento pela conta |
label | string | Nome exibido na tela, até 60 chars |
value | depende do campo | Já convertido para o tipo do campo: string, text, select e sequence → string; integer → número inteiro; decimal → string decimal ("12.5", mesmo padrão dos valores monetários da API); date → string YYYY-MM-DD; boolean → true/false |
read_only | boolean | true quando o valor está gravado mas o campo não aceita mais escrita — ver Campos somente leitura |
Regras de leitura:
- Campos sem valor são omitidos. O array traz apenas os campos preenchidos daquele contato — não conte com uma posição fixa nem com o array ter sempre o mesmo tamanho.
- A ordem não é garantida. Sempre localize o campo pelo
key. - A lista varia por conta e por
contact_type. Um campo definido só paracustomernão aparece em fornecedores.
Gravando campos personalizados
custom_fields é aceito em POST, PATCH e PUT, dentro do envelope contact. Dois formatos são aceitos:
{ "contact": { "custom_fields": { "matricula": "2026-0117", "turma": "3º ano B" } } }
{ "contact": { "custom_fields": [ { "key": "matricula", "value": "2026-0117" } ] } }
O segundo é o mesmo formato que o GET devolve, então você pode ler um contato e devolver o array inteiro num PATCH sem filtrar nada: as entradas read_only são ignoradas em silêncio.
| Situação | Resultado |
|---|---|
key que não existe na sua conta | 422 — a requisição inteira falha e nada é gravado |
key de um campo desativado, ou que não se aplica a este contact_type | Ignorado em silêncio. O valor gravado continua como está |
Valor em branco ("" ou null) | Apaga o valor do campo |
| Campo omitido do payload | Mantém o valor atual |
custom_fields omitido | Nenhum campo personalizado é tocado |
| Valor que é lista ou objeto | 422 |
Mesma key duas vezes no formato de array | 422 |
| Texto com caracteres de controle (byte nulo, etc.) | 422 |
| Mais de 100 campos numa requisição | 422 |
Validação por tipo
Um valor que não corresponde ao tipo do campo é recusado com 422 — nunca convertido em silêncio:
| Tipo | Aceita | Exemplos recusados |
|---|---|---|
string, text | Qualquer texto. string até 255 chars, text até 16.000 | Texto acima do limite |
select | Exatamente uma das opções configuradas | Qualquer valor fora da lista |
date | String ISO YYYY-MM-DD | 15/03/2026, 2026-02-30 |
boolean | true/false, ou "1"/"0", "true"/"false", "sim"/"nao", "yes"/"no", "on"/"off" | "talvez" |
integer | Inteiro não-negativo, como número ou string de dígitos | "12abc", "-5" |
decimal | Número, ou string em 1234.56 ou 1.234,56. Máximo 11 dígitos inteiros e 4 decimais | "12abc", 1000000000000 |
Campos marcados como únicos recusam com 422 um valor já usado por outro contato da mesma conta (a unicidade é por conta — o mesmo valor pode existir em outra conta).
Campos marcados como obrigatórios precisam vir preenchidos no POST; omiti-los ou enviá-los em branco retorna 422. No PATCH só são cobrados quando você os envia explicitamente em branco.
As mensagens de 422 são prefixadas pelo label do campo, para você saber qual falhou:
{ "status": 422, "errors": ["Turno não é válido", "Matrícula já está em uso"] }
Campos somente leitura
Um valor volta com read_only: true — e é ignorado se você enviá-lo de volta — em três situações:
- o campo foi desativado na tela depois que o valor já estava gravado;
- o campo deixou de valer para este
contact_type(por exemplo, passou a valer só para fornecedores, ou o contato mudou de tipo); - o campo é do tipo sequência, cujo valor o Sacador gera. Sequências configuradas como manuais aceitam um valor enquanto estiverem vazias; depois de preenchidas ficam congeladas, igual à tela.
Um valor manual de sequência empurra o contador da conta para a frente, para que o próximo número gerado não colida com ele. Por isso a API recusa com 422 um valor cujo número esteja muito além da numeração atual (mais de 1.000.000 à frente): seria um salto irreversível na numeração de toda a conta.
Endpoints
GET/v1/contacts
Lista os contatos da conta autenticada, ordenados por description ascendente.
Query params:
| Param | Tipo | Comportamento |
|---|---|---|
contact_type | customer | supplier | Filtra por tipo |
active | true | false | Filtra por status |
filter | string | Define o campo da busca: contact (default, busca por description), document (busca por CPF/CNPJ — só dígitos), email (busca por email começando com s), code (igualdade exata) |
s | string | Termo da busca, interpretado conforme filter |
tag_id | int | Retorna apenas os contatos que têm a tag informada. Ver tags |
external_id | string | Igualdade exata com o external_id do contato. Espaços nas pontas são ignorados |
document | string | Igualdade exata com o CPF ou CNPJ. A máscara é ignorada (529.982.247-25 e 52998224725 encontram o mesmo contato). Um valor sem nenhum dígito ou letra válida não encontra nada |
page, per_page | int | Padrão de paginação |
document e filter=document são coisas diferentes: filter=document&s=529982 é uma busca por prefixo (encontra todo documento que começa com 529982), enquanto document=529982 só encontra um contato cujo documento seja exatamente 529982, ou seja, nenhum. Para localizar um cliente específico pelo CPF/CNPJ, use document com o número completo.
Os filtros se combinam entre si (e com s/filter) como condições que precisam valer ao mesmo tempo.
Exemplo cURL (cliente pelo identificador do seu sistema):
curl -X GET 'https://api.sacador.com.br/v1/contacts?external_id=assoc-1234' \
-H 'Authorization: Bearer sct_seu_token_aqui'
Exemplo cURL:
curl -X GET 'https://api.sacador.com.br/v1/contacts?contact_type=customer&active=true&page=1' \
-H 'Authorization: Bearer sct_seu_token_aqui'
Resposta 200 OK:
{
"contacts": [
{
"id": 101,
"contact_type": "customer",
"code": "C-0042",
"active": true,
"email": "[email protected]",
"legal_entity": false,
"full_name": "Maria Silva",
"company_name": null,
"trademark": null,
"birth_date": "1990-03-15",
"cpf": "12345678901",
"cnpj": null,
"business_phone_area": "11",
"business_phone_number": "30001234",
"cell_phone_area": "11",
"cell_phone_number": "999998888",
"address_postal_code": "01310-100",
"address_street": "Avenida Paulista",
"address_number": "1000",
"address_complement": "Apto 42",
"address_district": "Bela Vista",
"address_city": "São Paulo",
"address_state": "SP",
"observation": null,
"external_id": "assoc-1234",
"on_account_limit": "300.0",
"store_credit_balance": "17.5",
"description": "Maria Silva",
"document": "12345678901",
"on_account_balance": "45.0",
"on_account_allowed": true,
"tags": [
{ "id": 12, "name": "Mensalistas", "color": "success" }
],
"custom_fields": [
{ "key": "matricula", "label": "Matrícula", "value": "2026-0117", "read_only": false },
{ "key": "turma", "label": "Turma", "value": "3º ano B", "read_only": false }
],
"created_at": "2026-04-01T10:00:00.000Z",
"updated_at": "2026-04-01T10:00:00.000Z"
}
]
}
GET/v1/contacts/:id
Exemplo cURL:
curl -X GET 'https://api.sacador.com.br/v1/contacts/101' \
-H 'Authorization: Bearer sct_seu_token_aqui'
Resposta 200 OK: objeto idêntico ao item da listagem.
Erros: 404 se inexistente ou de outra conta.
POST/v1/contacts
Cria um contato.
Body (envelopado em contact):
{
"contact": {
"contact_type": "customer",
"legal_entity": false,
"full_name": "Maria Silva",
"cpf": "123.456.789-01",
"email": "[email protected]",
"address_postal_code": "01310-100",
"address_street": "Avenida Paulista",
"address_number": "1000",
"address_district": "Bela Vista",
"address_city": "São Paulo",
"address_state": "sp"
}
}
Campos aceitos (qualquer outro campo enviado é ignorado, incluindo account_id):
contact_type, code, active, email, legal_entity, full_name, birth_date, cpf, cnpj, company_name, trademark, business_phone_area, business_phone_number, cell_phone_area, cell_phone_number, address_postal_code, address_street, address_number, address_complement, address_district, address_city, address_state, observation, external_id, on_account_limit, tag_ids, custom_fields.
store_credit_balance, on_account_balance e on_account_allowed são somente leitura: enviados, são ignorados. O saldo de créditos muda por POST/v1/store_credits.
on_account_limit segue o formato descrito em Limite de fiado: reais com ponto decimal, até duas casas.
tag_ids é um array de ids de tags da conta e substitui o conjunto atual de tags do contato. Detalhes e casos de borda em tags.
custom_fields grava os campos personalizados da conta. Formatos aceitos, validações e casos de borda em Campos personalizados.
Normalização aplicada pelo servidor
cpf: removidos todos os caracteres não-dígito.cnpj: removidos caracteres fora de[A-Z0-9]e convertido para maiúsculas.email: minúsculo e sem espaços.address_state: maiúsculo.full_name,company_name,trademark,address_*: espaços em excesso são colapsados (squishize).
Exemplo cURL (pessoa física):
curl -X POST 'https://api.sacador.com.br/v1/contacts' \
-H 'Authorization: Bearer sct_seu_token_aqui' \
-H 'Content-Type: application/json' \
-d '{
"contact": {
"contact_type": "customer",
"legal_entity": false,
"full_name": "Maria Silva",
"cpf": "123.456.789-01",
"email": "[email protected]",
"cell_phone_area": "11",
"cell_phone_number": "999998888",
"address_postal_code": "01310-100",
"address_street": "Avenida Paulista",
"address_number": "1000",
"address_district": "Bela Vista",
"address_city": "São Paulo",
"address_state": "SP"
}
}'
Exemplo cURL (pessoa jurídica):
curl -X POST 'https://api.sacador.com.br/v1/contacts' \
-H 'Authorization: Bearer sct_seu_token_aqui' \
-H 'Content-Type: application/json' \
-d '{
"contact": {
"contact_type": "customer",
"legal_entity": true,
"company_name": "Acme Comércio Ltda",
"trademark": "Acme",
"cnpj": "12.345.678/0001-90",
"email": "[email protected]",
"address_postal_code": "04538-132",
"address_street": "Rua Funchal",
"address_number": "263",
"address_district": "Vila Olímpia",
"address_city": "São Paulo",
"address_state": "SP"
}
}'
Resposta 201 Created: corpo idêntico a um item de GET/v1/contacts (todos os campos do Modelo).
Erros 422:
- CPF/CNPJ inválido ou já cadastrado (mesma conta + mesmo contact_type)
- full_name com número ou pontuação (PF)
- company_name ou trademark ausente ou com caractere não aceito (PJ)
- Endereço incompleto
- Email mal formado
- external_id já usado por outro contato da conta
- on_account_limit negativo, com vírgula ou com mais de duas casas decimais
PATCH/v1/contacts/:id
Atualiza um contato. Mesmos campos do POST. Campos omitidos preservam o valor atual.
Exemplo cURL:
curl -X PATCH 'https://api.sacador.com.br/v1/contacts/101' \
-H 'Authorization: Bearer sct_seu_token_aqui' \
-H 'Content-Type: application/json' \
-d '{
"contact": {
"email": "[email protected]",
"cell_phone_number": "988887777"
}
}'
Resposta 200 OK: corpo idêntico a um item de GET/v1/contacts.
Erros: 404 (conta errada), 422 (validação).
DELETE/v1/contacts/:id
Exemplo cURL:
curl -X DELETE 'https://api.sacador.com.br/v1/contacts/101' \
-H 'Authorization: Bearer sct_seu_token_aqui'
Resposta 204 No Content.
Erros:
- 404 — contato inexistente ou de outra conta
- 422 — contato possui cobranças (billings), contas a pagar/receber (bills) ou lançamentos de caixa (cashiers) associados. A exclusão é bloqueada (restrict_with_error)