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/transfersDifference vs PIX Cash-Out
| Aspect | TEF (/transfers) | PIX Cash-Out (/pix/cash-out) |
|---|---|---|
| ISPB destination | Toujours 37839059 (Owem) | N'importe quel PSP |
endToEndId dans la reponse | N'existe pas | E{ISPB}{YYYYMMDDHHmm}{entropy} |
feeAmount | Toujours 0 | Des frais peuvent etre preleves |
| Reglement | Immediat via TigerBeetle | Asynchrone via SPI/BACEN |
| Famille de webhooks | tef.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
| Header | Type | Obligatoire | Description |
|---|---|---|---|
Authorization | String | Oui | ApiKey {client_id}:{client_secret} |
Content-Type | String | Oui | application/json |
hmac | String | Oui | Signature HMAC-SHA512 du body (hex) -- voir HMAC-SHA512 |
Idempotency-Key | String | Non | Cle 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
| Champ | Type | Obligatoire | Description |
|---|---|---|---|
amount | Integer | Oui | Valeur 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. |
description | String | Non | Description du transfert (max 140 caracteres) |
externalId | String | Non | Identifiant 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
| Champ | Type | Obligatoire | Description |
|---|---|---|---|
destinationKey | String | Oui | Cle PIX du compte Owem destination |
destinationKeyType | String | Oui | CPF | CNPJ | EMAIL | PHONE | EVP |
Mode B -- destination par agence + compte Owem
| Champ | Type | Obligatoire | Description |
|---|---|---|---|
destinationAgency | String | Oui | 4 chiffres (ex: 0001) |
destinationAccountNumber | String | Oui | Numero 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)
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)
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) :
{
"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.
| Champ | Type | Description |
|---|---|---|
worked | Boolean | true indique que la requete a ete acceptee |
final | Boolean | true quand la transaction a atteint un etat terminal (toujours true sur TEF regle) |
transactionId | String | Identifiant unique de la transaction (prefixe TEF) |
externalId | String | Votre identifiant, retourne tel qu'envoye. null si non fourni ou rejete |
amount | Integer | Valeur du transfert en unites de base (1 BRL = 10000). Pour convertir en BRL, divisez par 10000. |
feeAmount | Integer | Toujours 0 (TEF interne n'a pas de frais) |
netAmount | Integer | Egal a amount (sans frais) |
channel | String | Toujours "tef" |
status | String | "settled" lors du reglement immediat |
detail | String | Message descriptif |
Codes d'Erreur
Erreurs de validation (HTTP 422)
Shape de la reponse : {"status": "failed", "errors": [{"code": "<code>", "params": {...}}]}
| Code | Description |
|---|---|
route_via_pix_cashout | La destination n'est pas un client Owem. Utilisez POST /api/external/pix/cash-out. |
destination_required | Aucun mode de destination fourni (destinationKey+destinationKeyType ou destinationAgency+destinationAccountNumber). |
destination_ambiguous | A envoye destinationKey ET destinationAgency dans la meme requete. |
destination_not_found | Cle/compte destination n'existe pas ou est inactif chez Owem. params inclut account_number et agency. |
self_transfer | Le compte source est egal au compte destination. params.account_id contient l'ID. |
pix_key_ambiguous | Cle 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": {...}}]}
| Code | Description |
|---|---|
insufficient_balance | Solde disponible inferieur au amount. |
pix_out_transaction_limit_exceeded | Le montant depasse la limite par transaction configuree pour le compte. |
Erreurs d'input (HTTP 400)
Shape de la reponse : {"errors": {"bad_request": "<message>"}}
| Message | Cause |
|---|---|
invalid or missing amount | amount absent, zero, negatif ou non-entier. |
Erreurs d'authentification / autorisation
| HTTP | Shape | Description |
|---|---|---|
| 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).
{
"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.
{
"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).
{
"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
- PIX Cash-Out par Cle -- pour les transferts hors Owem.
- HMAC-SHA512 -- details de la signature.
- API Key -- comment configurer les permissions.
- Webhooks -- comment s'abonner et valider les evenements.