TEF -- Transferência entre Contas Owem
Transferência entre duas contas Owem (TEF). Sem rota BACEN, sem tarifa, settlement imediato via TigerBeetle.
Endpoint
POST /api/external/transfersDiferença vs PIX Cash-Out
| Aspecto | TEF (/transfers) | PIX Cash-Out (/pix/cash-out) |
|---|---|---|
| ISPB destino | Sempre 37839059 (Owem) | Qualquer PSP |
endToEndId no response | Não existe | E{ISPB}{YYYYMMDDHHmm}{entropy} |
feeAmount | Sempre 0 | Pode ser cobrada tarifa |
| Settlement | Imediato via TigerBeetle | Assíncrono via SPI/BACEN |
| Família de webhooks | tef.transfer.* | pix.payout.* |
Use este endpoint quando a chave ou a conta destino é da Owem Pay IP. Para qualquer outro PSP, use PIX Cash-Out.
Headers
| Header | Tipo | Obrigatório | Descrição |
|---|---|---|---|
Authorization | String | Sim | ApiKey {client_id}:{client_secret} |
Content-Type | String | Sim | application/json |
hmac | String | Sim | Assinatura HMAC-SHA512 do body (hex) -- veja HMAC-SHA512 |
Idempotency-Key | String | Não | Chave única para evitar processamento duplicado (max 256 chars) |
Ordem alfabética das chaves no body
A validação HMAC reorderna o JSON do body em ordem alfabética antes de comparar a assinatura. Serialize o body com as chaves em ordem alfabética ou a verificação HMAC falha com 401. Veja HMAC-SHA512.
Permissão obrigatória
A API Key deve ter a permission transfer:write. Sem ela, a requisição retorna 403 Forbidden.
Request Body
Aceita dois modos de destino mutuamente exclusivos:
Comum
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
amount | Integer | Sim | Valor em centavos no request. R$ 1,00 = 100. No response, o backend devolve em unidades base (1 BRL = 10000): enviar 100 retorna amount: 10000. |
description | String | Não | Descrição da transferência (max 140 caracteres) |
externalId | String | Não | Identificador do seu sistema. Max 128 chars após trim. Apenas caracteres a-zA-Z0-9._:-. Valores inválidos são silenciosamente descartados (null no retorno). |
Modo A -- destino por chave PIX Owem
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
destinationKey | String | Sim | Chave PIX da conta Owem destino |
destinationKeyType | String | Sim | CPF | CNPJ | EMAIL | PHONE | EVP |
Modo B -- destino por agência + conta Owem
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
destinationAgency | String | Sim | 4 dígitos (ex: 0001) |
destinationAccountNumber | String | Sim | Número da conta Owem destino |
Modos mutuamente exclusivos
Informar destinationKey E destinationAgency na mesma requisição retorna 422 destination_ambiguous. Não informar nenhum dos dois retorna 422 destination_required.
camelCase ou snake_case
O backend converte automaticamente camelCase para snake_case quando o header X-Key-Case: camelCase está presente. Você pode usar qualquer um dos dois formatos no body. A documentação canônica usa camelCase.
Exemplo (Modo A -- por chave)
curl -X POST https://api.owem.com.br/api/external/transfers \
-H "Authorization: ApiKey $CLIENT_ID:$CLIENT_SECRET" \
-H "Content-Type: application/json" \
-H "hmac: $HMAC" \
-H "Idempotency-Key: 6f9c2b3e-1d4a-4f8b-9c2d-1e2f3a4b5c6d" \
-d '{
"amount": 100,
"description": "Transferencia interna",
"destinationKey": "62188010000150",
"destinationKeyType": "CNPJ",
"externalId": "ord-2026-05-25-001"
}'Exemplo (Modo B -- por ag+conta)
curl -X POST https://api.owem.com.br/api/external/transfers \
-H "Authorization: ApiKey $CLIENT_ID:$CLIENT_SECRET" \
-H "Content-Type: application/json" \
-H "hmac: $HMAC" \
-H "Idempotency-Key: 6f9c2b3e-1d4a-4f8b-9c2d-1e2f3a4b5c6d" \
-d '{
"amount": 100,
"description": "Transferencia interna",
"destinationAgency": "0001",
"destinationAccountNumber": "10001",
"externalId": "ord-2026-05-25-002"
}'Calculando HMAC
A assinatura HMAC-SHA512 é computada sobre o body JSON serializado com as chaves em ordem alfabética. Veja HMAC-SHA512 para o algoritmo completo.
Resposta de Sucesso -- 200
Para uma requisição com amount: 100 (R$ 1,00):
{
"worked": true,
"final": true,
"transactionId": "TEFabcd1234...",
"externalId": "ord-2026-05-25-001",
"amount": 10000,
"feeAmount": 0,
"netAmount": 10000,
"channel": "tef",
"status": "settled",
"detail": "Settled in ledger"
}Valores no response em unidades base (1 BRL = 10000)
O request aceita amount em centavos (R$ 1,00 = 100), mas o response devolve amount, feeAmount e netAmount em unidades base (1 BRL = 10000). Para reais, divida por 10000 (ex.: amount: 10000 é R$ 1,00). Mesmo comportamento do endpoint PIX Cash-Out.
Header X-Key-Case: camelCase recomendado
Sem o header X-Key-Case: camelCase, o response vem em snake_case (transaction_id, external_id, fee_amount, net_amount). Envie o header em todas as requisições para receber camelCase como mostrado acima.
endToEndId não está presente
TEF não gera identificador End-to-End BACEN. Use transactionId (prefixo TEF) ou externalId para rastrear a transação.
| Campo | Tipo | Descrição |
|---|---|---|
worked | Boolean | true indica que a requisição foi aceita |
final | Boolean | true quando a transação atingiu estado terminal (sempre true em TEF settled) |
transactionId | String | Identificador único da transação (prefixo TEF) |
externalId | String | Seu identificador, retornado tal como enviado. null se não informado ou descartado |
amount | Integer | Valor da transferência em unidades base (1 BRL = 10000). Para reais, divida por 10000. |
feeAmount | Integer | Sempre 0 (TEF interno não tem tarifa) |
netAmount | Integer | Igual a amount (sem tarifa) |
channel | String | Sempre "tef" |
status | String | "settled" na liquidação imediata |
detail | String | Mensagem descritiva |
Códigos de Erro
Erros de validação (HTTP 422)
Response shape: {"status": "failed", "errors": [{"code": "<code>", "params": {...}}]}
| Código | Descrição |
|---|---|
route_via_pix_cashout | Destino não é cliente Owem. Use POST /api/external/pix/cash-out. |
destination_required | Nenhum dos modos de destino informado (destinationKey+destinationKeyType ou destinationAgency+destinationAccountNumber). |
destination_ambiguous | Informou destinationKey E destinationAgency na mesma requisição. |
destination_not_found | Chave/conta destino não existe ou está inativa na Owem. params inclui account_number e agency. |
self_transfer | A conta source é igual à conta destino. params.account_id traz o ID. |
pix_key_ambiguous | Chave de 11 dígitos sem destinationKeyType quando o valor é CPF válido com DDD válido e terceiro dígito 9 (pode ser CPF ou telefone). Envie destinationKeyType: "CPF" ou "PHONE". |
Erros de integração (HTTP 400)
Response shape: {"status": "failed", "errors": [{"code": "<code>", "params": {...}}]}
| Código | Descrição |
|---|---|
insufficient_balance | Saldo disponível menor que o amount. |
pix_out_transaction_limit_exceeded | Valor excede o limite por transação configurado para a conta. |
Erros de input (HTTP 400)
Response shape: {"errors": {"bad_request": "<message>"}}
| Mensagem | Causa |
|---|---|
invalid or missing amount | amount ausente, zero, negativo, ou não-inteiro. |
Erros de autenticação / autorização
| HTTP | Shape | Descrição |
|---|---|---|
| 401 | {"worked": false, "detail": "Missing HMAC header"} | Header hmac ausente. |
| 401 | {"worked": false, "detail": "Invalid HMAC signature"} | Assinatura HMAC não confere (body desordenado ou secret errado). |
| 401 | {"error": {"status": 401, "message": "..."}} | API Key ausente, inválida, inativa ou expirada. Confira a mensagem error.message. |
| 403 | {"error": {"status": 403, "message": "Request IP not in API key whitelist"}} | IP da requisição não está na whitelist da API Key. |
| 403 | {"errors": {"forbidden": "Permission required: transfer:write"}} | API Key sem a permission transfer:write. |
Webhooks
Quando a transferência liquida, o backend dispara dois webhooks (ambos best-effort). Todos os payloads recebem automaticamente eventType e status injetados pelo dispatcher: tef.transfer.sent e tef.transfer.received recebem status: "settled"; tef.transfer.failed recebe status: "failed". Valores monetários são em unidades base (1 BRL = 10000), iguais ao response do endpoint.
tef.transfer.sent
Disparado para os assinantes do caller (source account).
{
"eventType": "tef.transfer.sent",
"status": "settled",
"transactionId": "TEFabcd1234...",
"accountId": 10001,
"senderAccountId": 10001,
"receiverAccountId": 10002,
"amount": 10000,
"description": "Transferencia interna",
"merchantId": "e84b303c-007f-407d-ae20-f1056a24524d",
"entityId": "26a48541-edce-4581-8c6e-564e7f2e6cd7",
"settledAt": "2026-05-25T14:30:00Z"
}tef.transfer.received
Disparado para os assinantes do destinatário (receiver account). O transactionId tem sufixo _RCV para diferenciar a perna de crédito da de débito.
{
"eventType": "tef.transfer.received",
"status": "settled",
"transactionId": "TEFabcd1234..._RCV",
"accountId": 10002,
"senderAccountId": 10001,
"receiverAccountId": 10002,
"amount": 10000,
"description": "Transferencia interna",
"merchantId": "e84b303c-007f-407d-ae20-f1056a24524d",
"entityId": "26a48541-edce-4581-8c6e-564e7f2e6cd7",
"settledAt": "2026-05-25T14:30:00Z"
}tef.transfer.failed
Disparado se a settle falhar no TigerBeetle (raro).
{
"eventType": "tef.transfer.failed",
"status": "failed",
"accountId": 10001,
"transactionId": "TEFabcd1234...",
"receiverAccountId": 10002,
"amount": 10000,
"merchantId": "e84b303c-007f-407d-ae20-f1056a24524d",
"entityId": "26a48541-edce-4581-8c6e-564e7f2e6cd7",
"failureReason": "tb error description",
"failedAt": "2026-05-25T14:30:00Z"
}Idempotência
A idempotência é controlada exclusivamente pelo header Idempotency-Key.
- Header
Idempotency-Key(recomendado, UUID): chave única por requisição. O servidor armazena a resposta por 24h e, em qualquer retry com a mesma chave + mesmo método + mesmo path, retorna o corpo cacheado com o status HTTP original (200 ou 202) e dois headers de resposta:idempotency-key: <chave enviada>x-idempotent-replay: true
- O servidor não compara o body do request no replay: o body retornado é sempre o da primeira chamada bem-sucedida que populou o cache. Use chaves distintas para requisições distintas.
Idempotency-Key não retorna 409
Diferente do que algumas integrações esperam, o servidor não responde 409 quando a chave já foi usada. Em vez disso, devolve o mesmo response da primeira chamada (200/202 com o body original, mais o header x-idempotent-replay: true). Trate replays olhando para esse header, não esperando um status code diferente.
clientRequestId no body não é honrado em TEF
O endpoint /transfers aceita clientRequestId no body sem reclamar (o backend não rejeita o campo), mas não o usa como idempotência. Cada requisição com o mesmo clientRequestId e Idempotency-Key diferente gera uma transação independente. Use exclusivamente o header Idempotency-Key para garantir idempotência.
Próximos Passos
- PIX Cash-Out por Chave -- para transferências fora da Owem.
- HMAC-SHA512 -- detalhes da assinatura.
- API Key -- como configurar permissions.
- Webhooks -- como assinar e validar eventos.