TEF -- Transferencia entre Cuentas Owem
Transferencia entre dos cuentas Owem (TEF). Sin ruta BACEN, sin tarifa, liquidacion inmediata via TigerBeetle.
Endpoint
POST /api/external/transfersDiferencia vs PIX Cash-Out
| Aspecto | TEF (/transfers) | PIX Cash-Out (/pix/cash-out) |
|---|---|---|
| ISPB destino | Siempre 37839059 (Owem) | Cualquier PSP |
endToEndId en la respuesta | No existe | E{ISPB}{YYYYMMDDHHmm}{entropy} |
feeAmount | Siempre 0 | Puede cobrarse tarifa |
| Liquidacion | Inmediata via TigerBeetle | Asincrona via SPI/BACEN |
| Familia de webhooks | tef.transfer.* | pix.payout.* |
Use este endpoint cuando la clave o cuenta destino es de Owem Pay IP. Para cualquier otro PSP, use PIX Cash-Out.
Headers
| Header | Tipo | Obligatorio | Descripcion |
|---|---|---|---|
Authorization | String | Si | ApiKey {client_id}:{client_secret} |
Content-Type | String | Si | application/json |
hmac | String | Si | Firma HMAC-SHA512 del body (hex) -- ver HMAC-SHA512 |
Idempotency-Key | String | No | Clave unica para evitar procesamiento duplicado (max 256 chars) |
Orden alfabetico de las claves en el body
La validacion HMAC reordena el JSON del body en orden alfabetico antes de comparar la firma. Serialice el body con las claves en orden alfabetico o la verificacion HMAC falla con 401. Ver HMAC-SHA512.
Permiso obligatorio
La API Key debe tener el permiso transfer:write. Sin el, la solicitud retorna 403 Forbidden.
Request Body
Acepta dos modos de destino mutuamente excluyentes:
Comun
| Campo | Tipo | Obligatorio | Descripcion |
|---|---|---|---|
amount | Integer | Si | Valor en centavos en el request. R$ 1,00 = 100. En la respuesta el backend devuelve en unidades base (1 BRL = 10000): enviar 100 retorna amount: 10000. |
description | String | No | Descripcion de la transferencia (max 140 caracteres) |
externalId | String | No | Identificador de su sistema. Max 128 chars despues de trim. Solo caracteres a-zA-Z0-9._:-. Valores invalidos se descartan silenciosamente (null en la respuesta). |
Modo A -- destino por clave PIX Owem
| Campo | Tipo | Obligatorio | Descripcion |
|---|---|---|---|
destinationKey | String | Si | Clave PIX de la cuenta Owem destino |
destinationKeyType | String | Si | CPF | CNPJ | EMAIL | PHONE | EVP |
Modo B -- destino por agencia + cuenta Owem
| Campo | Tipo | Obligatorio | Descripcion |
|---|---|---|---|
destinationAgency | String | Si | 4 digitos (ej: 0001) |
destinationAccountNumber | String | Si | Numero de la cuenta Owem destino |
Modos mutuamente excluyentes
Enviar destinationKey Y destinationAgency en la misma solicitud retorna 422 destination_ambiguous. No enviar ninguno retorna 422 destination_required.
camelCase o snake_case
El backend convierte camelCase a snake_case automaticamente cuando el header X-Key-Case: camelCase esta presente. Puede usar cualquiera de los dos formatos en el body. La documentacion canonica usa camelCase.
Ejemplo (Modo A -- por clave)
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"
}'Ejemplo (Modo B -- por agencia+cuenta)
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
La firma HMAC-SHA512 se calcula sobre el body JSON serializado con las claves en orden alfabetico. Ver HMAC-SHA512 para el algoritmo completo.
Respuesta de Exito -- 200
Para una solicitud con 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 en la respuesta en unidades base (1 BRL = 10000)
La solicitud acepta amount en centavos (R$ 1,00 = 100), pero la respuesta retorna amount, feeAmount y netAmount en unidades base (1 BRL = 10000). Para BRL, divida por 10000 (ej: amount: 10000 es R$ 1,00). Mismo comportamiento del endpoint PIX Cash-Out.
Header X-Key-Case: camelCase recomendado
Sin el header X-Key-Case: camelCase, la respuesta retorna en snake_case (transaction_id, external_id, fee_amount, net_amount). Envie el header en todas las solicitudes para recibir camelCase como se muestra arriba.
endToEndId no esta presente
TEF no genera identificador End-to-End BACEN. Use transactionId (prefijo TEF) o externalId para rastrear la transaccion.
| Campo | Tipo | Descripcion |
|---|---|---|
worked | Boolean | true indica que la solicitud fue aceptada |
final | Boolean | true cuando la transaccion ha alcanzado estado terminal (siempre true en TEF liquidado) |
transactionId | String | Identificador unico de la transaccion (prefijo TEF) |
externalId | String | Su identificador, retornado tal como enviado. null si no fue informado o descartado |
amount | Integer | Valor de la transferencia en unidades base (1 BRL = 10000). Para BRL, divida por 10000. |
feeAmount | Integer | Siempre 0 (TEF interno no tiene tarifa) |
netAmount | Integer | Igual a amount (sin tarifa) |
channel | String | Siempre "tef" |
status | String | "settled" en liquidacion inmediata |
detail | String | Mensaje descriptivo |
Codigos de Error
Errores de validacion (HTTP 422)
Shape de la respuesta: {"status": "failed", "errors": [{"code": "<code>", "params": {...}}]}
| Codigo | Descripcion |
|---|---|
route_via_pix_cashout | El destino no es cliente Owem. Use POST /api/external/pix/cash-out. |
destination_required | Ningun modo de destino informado (destinationKey+destinationKeyType o destinationAgency+destinationAccountNumber). |
destination_ambiguous | Envio destinationKey Y destinationAgency en la misma solicitud. |
destination_not_found | Clave/cuenta destino no existe o esta inactiva en Owem. params incluye account_number y agency. |
self_transfer | La cuenta source es igual a la cuenta destino. params.account_id trae el ID. |
pix_key_ambiguous | Clave de 11 digitos sin destinationKeyType cuando el valor es un CPF valido con DDD valido y tercer digito 9 (puede ser CPF o telefono). Envie destinationKeyType: "CPF" o "PHONE". |
Errores de integracion (HTTP 400)
Shape de la respuesta: {"status": "failed", "errors": [{"code": "<code>", "params": {...}}]}
| Codigo | Descripcion |
|---|---|
insufficient_balance | Saldo disponible menor que el amount. |
pix_out_transaction_limit_exceeded | Valor excede el limite por transaccion configurado para la cuenta. |
Errores de input (HTTP 400)
Shape de la respuesta: {"errors": {"bad_request": "<message>"}}
| Mensaje | Causa |
|---|---|
invalid or missing amount | amount ausente, cero, negativo, o no-entero. |
Errores de autenticacion / autorizacion
| HTTP | Shape | Descripcion |
|---|---|---|
| 401 | {"worked": false, "detail": "Missing HMAC header"} | Header hmac ausente. |
| 401 | {"worked": false, "detail": "Invalid HMAC signature"} | Firma HMAC no coincide (body desordenado o secret incorrecto). |
| 401 | {"error": {"status": 401, "message": "..."}} | API Key ausente, invalida, inactiva o expirada. Verifique el mensaje error.message. |
| 403 | {"error": {"status": 403, "message": "Request IP not in API key whitelist"}} | IP de la solicitud no esta en la whitelist de la API Key. |
| 403 | {"errors": {"forbidden": "Permission required: transfer:write"}} | API Key sin el permiso transfer:write. |
Webhooks
Cuando la transferencia se liquida, el backend dispara dos webhooks (ambos best-effort). Todos los payloads reciben automaticamente eventType y status inyectados por el dispatcher: tef.transfer.sent y tef.transfer.received reciben status: "settled"; tef.transfer.failed recibe status: "failed". Los valores monetarios estan en unidades base (1 BRL = 10000), iguales a la respuesta del endpoint.
tef.transfer.sent
Disparado para los suscriptores del caller (cuenta source).
{
"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 los suscriptores del destinatario (cuenta receiver). El transactionId tiene sufijo _RCV para diferenciar la pierna de credito de la de debito.
{
"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 si la liquidacion falla en 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"
}Idempotencia
La idempotencia se controla exclusivamente por el header Idempotency-Key.
- Header
Idempotency-Key(recomendado, UUID): clave unica por solicitud. El servidor almacena la respuesta por 24h y, en cualquier reintento con la misma clave + metodo + path, retorna el body cacheado con el status HTTP original (200 o 202) mas dos headers de respuesta:idempotency-key: <clave enviada>x-idempotent-replay: true
- El servidor no compara el body de la solicitud en el replay: el body retornado es siempre el de la primera llamada exitosa que populo el cache. Use claves distintas para solicitudes distintas.
Idempotency-Key no retorna 409
A diferencia de lo que algunas integraciones esperan, el servidor no responde 409 cuando la clave ya fue usada. En su lugar, devuelve la misma respuesta de la primera llamada (200/202 con el body original, mas el header x-idempotent-replay: true). Detecte replays inspeccionando ese header, no esperando un status code diferente.
clientRequestId en el body no es honrado en TEF
El endpoint /transfers acepta clientRequestId en el body sin reclamar (el backend no rechaza el campo), pero no lo usa como idempotencia. Cada solicitud con el mismo clientRequestId y diferente Idempotency-Key genera una transaccion independiente. Use exclusivamente el header Idempotency-Key para garantizar idempotencia.
Siguientes Pasos
- PIX Cash-Out por Clave -- para transferencias fuera de Owem.
- HMAC-SHA512 -- detalles de la firma.
- API Key -- como configurar permisos.
- Webhooks -- como suscribirse y validar eventos.