Skip to content

TEF -- Transfert entre Comptes Owem

Transfert entre deux comptes Owem (TEF). Pas de routage BACEN, pas de frais, reglement immediat via TigerBeetle.

Endpoint

POST /api/external/transfers

Difference vs PIX Cash-Out

AspectTEF (/transfers)PIX Cash-Out (/pix/cash-out)
ISPB destinationToujours 37839059 (Owem)N'importe quel PSP
endToEndId dans la reponseN'existe pasE{ISPB}{YYYYMMDDHHmm}{entropy}
feeAmountToujours 0Des frais peuvent etre preleves
ReglementImmediat via TigerBeetleAsynchrone via SPI/BACEN
Famille de webhookstef.transfer.*pix.payout.*

Utilisez ce endpoint quand la cle ou le compte destination est chez Owem Pay IP. Pour tout autre PSP, utilisez PIX Cash-Out.

En-tetes

HeaderTypeObligatoireDescription
AuthorizationStringOuiApiKey {client_id}:{client_secret}
Content-TypeStringOuiapplication/json
hmacStringOuiSignature HMAC-SHA512 du body (hex) -- voir HMAC-SHA512
Idempotency-KeyStringNonCle unique pour eviter le traitement en double (max 256 chars)

Ordre alphabetique des cles dans le body

La validation HMAC reordonne le JSON du body par ordre alphabetique avant de comparer la signature. Serialisez le body avec les cles en ordre alphabetique sinon la verification HMAC echoue avec 401. Voir HMAC-SHA512.

Permission obligatoire

L'API Key doit avoir la permission transfer:write. Sans elle, la requete retourne 403 Forbidden.

Request Body

Accepte deux modes de destination mutuellement exclusifs:

Commun

ChampTypeObligatoireDescription
amountIntegerOuiValeur en centavos dans la requete. R$ 1,00 = 100. Dans la reponse, le backend renvoie en unites de base (1 BRL = 10000) : envoyer 100 retourne amount: 10000.
descriptionStringNonDescription du transfert (max 140 caracteres)
externalIdStringNonIdentifiant de votre systeme. Max 128 chars apres trim. Seulement caracteres a-zA-Z0-9._:-. Les valeurs invalides sont silencieusement rejetees (null dans la reponse).

Mode A -- destination par cle PIX Owem

ChampTypeObligatoireDescription
destinationKeyStringOuiCle PIX du compte Owem destination
destinationKeyTypeStringOuiCPF | CNPJ | EMAIL | PHONE | EVP

Mode B -- destination par agence + compte Owem

ChampTypeObligatoireDescription
destinationAgencyStringOui4 chiffres (ex: 0001)
destinationAccountNumberStringOuiNumero du compte Owem destination

Modes mutuellement exclusifs

Envoyer destinationKey ET destinationAgency dans la meme requete retourne 422 destination_ambiguous. N'envoyer aucun des deux retourne 422 destination_required.

camelCase ou snake_case

Le backend convertit camelCase en snake_case automatiquement lorsque le header X-Key-Case: camelCase est present. Vous pouvez utiliser l'un ou l'autre format dans le body. La documentation canonique utilise camelCase.

Exemple (Mode A -- par cle)

bash
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": "Transfert interne",
    "destinationKey": "62188010000150",
    "destinationKeyType": "CNPJ",
    "externalId": "ord-2026-05-25-001"
  }'

Exemple (Mode B -- par agence+compte)

bash
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": "Transfert interne",
    "destinationAgency": "0001",
    "destinationAccountNumber": "10001",
    "externalId": "ord-2026-05-25-002"
  }'

Calcul du HMAC

La signature HMAC-SHA512 est calculee sur le body JSON serialise avec les cles en ordre alphabetique. Voir HMAC-SHA512 pour l'algorithme complet.

Reponse de Succes -- 200

Pour une requete avec amount: 100 (R$ 1,00) :

json
{
  "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"
}

Valeurs de la reponse en unites de base (1 BRL = 10000)

La requete accepte amount en centavos (R$ 1,00 = 100), mais la reponse retourne amount, feeAmount et netAmount en unites de base (1 BRL = 10000). Pour convertir en BRL, divisez par 10000 (ex : amount: 10000 vaut R$ 1,00). Meme comportement que le endpoint PIX Cash-Out.

Header X-Key-Case: camelCase recommande

Sans le header X-Key-Case: camelCase, la reponse revient en snake_case (transaction_id, external_id, fee_amount, net_amount). Envoyez ce header dans chaque requete pour recevoir camelCase comme montre ci-dessus.

endToEndId n'est pas present

TEF ne genere pas d'identifiant End-to-End BACEN. Utilisez transactionId (prefixe TEF) ou externalId pour suivre la transaction.

ChampTypeDescription
workedBooleantrue indique que la requete a ete acceptee
finalBooleantrue quand la transaction a atteint un etat terminal (toujours true sur TEF regle)
transactionIdStringIdentifiant unique de la transaction (prefixe TEF)
externalIdStringVotre identifiant, retourne tel qu'envoye. null si non fourni ou rejete
amountIntegerValeur du transfert en unites de base (1 BRL = 10000). Pour convertir en BRL, divisez par 10000.
feeAmountIntegerToujours 0 (TEF interne n'a pas de frais)
netAmountIntegerEgal a amount (sans frais)
channelStringToujours "tef"
statusString"settled" lors du reglement immediat
detailStringMessage descriptif

Codes d'Erreur

Erreurs de validation (HTTP 422)

Shape de la reponse : {"status": "failed", "errors": [{"code": "<code>", "params": {...}}]}

CodeDescription
route_via_pix_cashoutLa destination n'est pas un client Owem. Utilisez POST /api/external/pix/cash-out.
destination_requiredAucun mode de destination fourni (destinationKey+destinationKeyType ou destinationAgency+destinationAccountNumber).
destination_ambiguousA envoye destinationKey ET destinationAgency dans la meme requete.
destination_not_foundCle/compte destination n'existe pas ou est inactif chez Owem. params inclut account_number et agency.
self_transferLe compte source est egal au compte destination. params.account_id contient l'ID.
pix_key_ambiguousCle de 11 chiffres sans destinationKeyType lorsque la valeur est un CPF valide avec DDD valide et troisieme chiffre 9 (peut etre CPF ou telephone). Envoyez destinationKeyType: "CPF" ou "PHONE".

Erreurs d'integration (HTTP 400)

Shape de la reponse : {"status": "failed", "errors": [{"code": "<code>", "params": {...}}]}

CodeDescription
insufficient_balanceSolde disponible inferieur au amount.
pix_out_transaction_limit_exceededLe montant depasse la limite par transaction configuree pour le compte.

Erreurs d'input (HTTP 400)

Shape de la reponse : {"errors": {"bad_request": "<message>"}}

MessageCause
invalid or missing amountamount absent, zero, negatif ou non-entier.

Erreurs d'authentification / autorisation

HTTPShapeDescription
401{"worked": false, "detail": "Missing HMAC header"}Header hmac absent.
401{"worked": false, "detail": "Invalid HMAC signature"}Signature HMAC incorrecte (body desordonne ou secret errone).
401{"error": {"status": 401, "message": "..."}}API Key absente, invalide, inactive ou expiree. Verifiez error.message.
403{"error": {"status": 403, "message": "Request IP not in API key whitelist"}}IP de la requete pas dans la whitelist de l'API Key.
403{"errors": {"forbidden": "Permission required: transfer:write"}}API Key sans la permission transfer:write.

Webhooks

Quand le transfert se regle, le backend declenche deux webhooks (les deux best-effort). Chaque payload est automatiquement enrichi avec eventType et status injectes par le dispatcher : tef.transfer.sent et tef.transfer.received recoivent status: "settled" ; tef.transfer.failed recoit status: "failed". Les valeurs monetaires sont en unites de base (1 BRL = 10000), identiques a la reponse du endpoint.

tef.transfer.sent

Declenche pour les abonnes du caller (compte source).

json
{
  "eventType": "tef.transfer.sent",
  "status": "settled",
  "transactionId": "TEFabcd1234...",
  "accountId": 10001,
  "senderAccountId": 10001,
  "receiverAccountId": 10002,
  "amount": 10000,
  "description": "Transfert interne",
  "merchantId": "e84b303c-007f-407d-ae20-f1056a24524d",
  "entityId": "26a48541-edce-4581-8c6e-564e7f2e6cd7",
  "settledAt": "2026-05-25T14:30:00Z"
}

tef.transfer.received

Declenche pour les abonnes du destinataire (compte receiver). Le transactionId a le suffixe _RCV pour differencier la jambe de credit de celle de debit.

json
{
  "eventType": "tef.transfer.received",
  "status": "settled",
  "transactionId": "TEFabcd1234..._RCV",
  "accountId": 10002,
  "senderAccountId": 10001,
  "receiverAccountId": 10002,
  "amount": 10000,
  "description": "Transfert interne",
  "merchantId": "e84b303c-007f-407d-ae20-f1056a24524d",
  "entityId": "26a48541-edce-4581-8c6e-564e7f2e6cd7",
  "settledAt": "2026-05-25T14:30:00Z"
}

tef.transfer.failed

Declenche si le reglement echoue dans TigerBeetle (rare).

json
{
  "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"
}

Idempotence

L'idempotence est controlee exclusivement par le header Idempotency-Key.

  • Header Idempotency-Key (recommande, UUID) : cle unique par requete. Le serveur stocke la reponse pendant 24h et, sur tout retry avec la meme cle + methode + path, retourne le body cache avec le status HTTP original (200 ou 202) plus deux headers de reponse :
    • idempotency-key: <cle envoyee>
    • x-idempotent-replay: true
  • Le serveur ne compare pas le body de la requete au replay : le body retourne est toujours celui de la premiere appel reussi qui a peuple le cache. Utilisez des cles distinctes pour des requetes distinctes.

Idempotency-Key ne retourne pas 409

Contrairement a ce que certaines integrations attendent, le serveur ne repond pas 409 quand la cle a deja ete utilisee. Au lieu de cela, il retourne la meme reponse que le premier appel (200/202 avec le body original, plus le header x-idempotent-replay: true). Detectez les replays en inspectant ce header, pas en attendant un status code different.

clientRequestId dans le body n'est pas honore en TEF

Le endpoint /transfers accepte clientRequestId dans le body sans broncher (le backend ne rejette pas le champ), mais ne l'utilise pas comme idempotence. Chaque requete avec le meme clientRequestId mais un Idempotency-Key different produit une transaction independante. Utilisez exclusivement le header Idempotency-Key pour garantir l'idempotence.

Prochaines Etapes

Owem Pay Instituição de Pagamento — ISPB 37839059