TEF -- Transfer Between Owem Accounts
Transfer between two Owem accounts (TEF). No BACEN routing, no fee, immediate settlement via TigerBeetle.
Endpoint
POST /api/external/transfersDifference vs PIX Cash-Out
| Aspect | TEF (/transfers) | PIX Cash-Out (/pix/cash-out) |
|---|---|---|
| Destination ISPB | Always 37839059 (Owem) | Any PSP |
endToEndId in response | Does not exist | E{ISPB}{YYYYMMDDHHmm}{entropy} |
feeAmount | Always 0 | Fee may be charged |
| Settlement | Immediate via TigerBeetle | Asynchronous via SPI/BACEN |
| Webhook family | tef.transfer.* | pix.payout.* |
Use this endpoint when the destination key or account is at Owem Pay IP. For any other PSP, use PIX Cash-Out.
Headers
| Header | Type | Required | Description |
|---|---|---|---|
Authorization | String | Yes | ApiKey {client_id}:{client_secret} |
Content-Type | String | Yes | application/json |
hmac | String | Yes | HMAC-SHA512 signature of the body (hex) -- see HMAC-SHA512 |
Idempotency-Key | String | No | Unique key to prevent duplicate processing (max 256 chars) |
Alphabetical key order in body
HMAC validation reorders the JSON body alphabetically before comparing the signature. Serialize the body with keys in alphabetical order or HMAC verification fails with 401. See HMAC-SHA512.
Required permission
The API Key must have the transfer:write permission. Without it, the request returns 403 Forbidden.
Request Body
Accepts two mutually exclusive destination modes:
Common
| Field | Type | Required | Description |
|---|---|---|---|
amount | Integer | Yes | Amount in centavos in the request. R$ 1.00 = 100. In the response the backend returns base units (1 BRL = 10000): sending 100 returns amount: 10000. |
description | String | No | Transfer description (max 140 characters) |
externalId | String | No | Your system identifier. Max 128 chars after trim. Only a-zA-Z0-9._:- characters. Invalid values are silently discarded (null in the response). |
Mode A -- destination by Owem PIX key
| Field | Type | Required | Description |
|---|---|---|---|
destinationKey | String | Yes | PIX key of the destination Owem account |
destinationKeyType | String | Yes | CPF | CNPJ | EMAIL | PHONE | EVP |
Mode B -- destination by Owem agency + account
| Field | Type | Required | Description |
|---|---|---|---|
destinationAgency | String | Yes | 4 digits (e.g. 0001) |
destinationAccountNumber | String | Yes | Destination Owem account number |
Mutually exclusive modes
Sending destinationKey AND destinationAgency in the same request returns 422 destination_ambiguous. Sending neither returns 422 destination_required.
camelCase or snake_case
The backend converts camelCase to snake_case automatically when the X-Key-Case: camelCase header is present. You can use either format in the body. The canonical documentation uses camelCase.
Example (Mode A -- by key)
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": "Internal transfer",
"destinationKey": "62188010000150",
"destinationKeyType": "CNPJ",
"externalId": "ord-2026-05-25-001"
}'Example (Mode B -- by agency+account)
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": "Internal transfer",
"destinationAgency": "0001",
"destinationAccountNumber": "10001",
"externalId": "ord-2026-05-25-002"
}'Computing HMAC
The HMAC-SHA512 signature is computed over the JSON body serialized with keys in alphabetical order. See HMAC-SHA512 for the full algorithm.
Success Response -- 200
For a request with 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"
}Response values in base units (1 BRL = 10000)
The request accepts amount in centavos (R$ 1.00 = 100), but the response returns amount, feeAmount and netAmount in base units (1 BRL = 10000). To convert to BRL, divide by 10000 (e.g. amount: 10000 is R$ 1.00). Same behavior as the PIX Cash-Out endpoint.
Header X-Key-Case: camelCase recommended
Without the X-Key-Case: camelCase header, the response comes back in snake_case (transaction_id, external_id, fee_amount, net_amount). Send the header on every request to receive camelCase as shown above.
endToEndId is not present
TEF does not generate a BACEN End-to-End identifier. Use transactionId (prefixed TEF) or externalId to track the transaction.
| Field | Type | Description |
|---|---|---|
worked | Boolean | true indicates the request was accepted |
final | Boolean | true when the transaction has reached a terminal state (always true on settled TEF) |
transactionId | String | Unique transaction identifier (prefix TEF) |
externalId | String | Your identifier, returned as sent. null if not provided or discarded |
amount | Integer | Transfer amount in base units (1 BRL = 10000). To convert to BRL, divide by 10000. |
feeAmount | Integer | Always 0 (internal TEF has no fee) |
netAmount | Integer | Equals amount (no fee) |
channel | String | Always "tef" |
status | String | "settled" on immediate settlement |
detail | String | Descriptive message |
Error Codes
Validation errors (HTTP 422)
Response shape: {"status": "failed", "errors": [{"code": "<code>", "params": {...}}]}
| Code | Description |
|---|---|
route_via_pix_cashout | Destination is not an Owem customer. Use POST /api/external/pix/cash-out. |
destination_required | No destination mode provided (destinationKey+destinationKeyType or destinationAgency+destinationAccountNumber). |
destination_ambiguous | Sent destinationKey AND destinationAgency in the same request. |
destination_not_found | Destination key/account does not exist or is inactive at Owem. params includes account_number and agency. |
self_transfer | Source account equals destination account. params.account_id carries the ID. |
pix_key_ambiguous | 11-digit key without destinationKeyType when the value is a valid CPF with a valid DDD and third digit 9 (could be CPF or phone). Send destinationKeyType: "CPF" or "PHONE" to disambiguate. |
Integration errors (HTTP 400)
Response shape: {"status": "failed", "errors": [{"code": "<code>", "params": {...}}]}
| Code | Description |
|---|---|
insufficient_balance | Available balance less than amount. |
pix_out_transaction_limit_exceeded | Amount exceeds the per-transaction limit configured for the account. |
Input errors (HTTP 400)
Response shape: {"errors": {"bad_request": "<message>"}}
| Message | Cause |
|---|---|
invalid or missing amount | amount missing, zero, negative, or non-integer. |
Authentication / authorization errors
| HTTP | Shape | Description |
|---|---|---|
| 401 | {"worked": false, "detail": "Missing HMAC header"} | hmac header missing. |
| 401 | {"worked": false, "detail": "Invalid HMAC signature"} | HMAC signature does not match (unordered body or wrong secret). |
| 401 | {"error": {"status": 401, "message": "..."}} | API Key missing, invalid, inactive or expired. Read error.message for details. |
| 403 | {"error": {"status": 403, "message": "Request IP not in API key whitelist"}} | Request IP is not on the API Key whitelist. |
| 403 | {"errors": {"forbidden": "Permission required: transfer:write"}} | API Key without the transfer:write permission. |
Webhooks
When the transfer settles, the backend dispatches two webhooks (both best-effort). Every payload is automatically enriched with eventType and status by the dispatcher: tef.transfer.sent and tef.transfer.received receive status: "settled"; tef.transfer.failed receives status: "failed". Monetary values are in base units (1 BRL = 10000), same as the endpoint response.
tef.transfer.sent
Dispatched to the caller's subscribers (source account).
{
"eventType": "tef.transfer.sent",
"status": "settled",
"transactionId": "TEFabcd1234...",
"accountId": 10001,
"senderAccountId": 10001,
"receiverAccountId": 10002,
"amount": 10000,
"description": "Internal transfer",
"merchantId": "e84b303c-007f-407d-ae20-f1056a24524d",
"entityId": "26a48541-edce-4581-8c6e-564e7f2e6cd7",
"settledAt": "2026-05-25T14:30:00Z"
}tef.transfer.received
Dispatched to the destination's subscribers (receiver account). The transactionId has suffix _RCV to differentiate the credit leg from the debit leg.
{
"eventType": "tef.transfer.received",
"status": "settled",
"transactionId": "TEFabcd1234..._RCV",
"accountId": 10002,
"senderAccountId": 10001,
"receiverAccountId": 10002,
"amount": 10000,
"description": "Internal transfer",
"merchantId": "e84b303c-007f-407d-ae20-f1056a24524d",
"entityId": "26a48541-edce-4581-8c6e-564e7f2e6cd7",
"settledAt": "2026-05-25T14:30:00Z"
}tef.transfer.failed
Dispatched if settle fails in 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"
}Idempotency
Idempotency is controlled exclusively via the Idempotency-Key header.
- Header
Idempotency-Key(recommended, UUID): unique key per request. The server stores the response for 24h and, on any retry with the same key + method + path, returns the cached body with the original HTTP status (200 or 202) plus two response headers:idempotency-key: <key sent>x-idempotent-replay: true
- The server does not compare the request body on replay: the body returned is always the one from the first successful call that populated the cache. Use distinct keys for distinct requests.
Idempotency-Key does not return 409
Unlike some integrations expect, the server does not respond with 409 when the key has already been used. Instead, it returns the same response as the first call (200/202 with the original body, plus the x-idempotent-replay: true header). Detect replays by inspecting that header, not by expecting a different status code.
clientRequestId in the body is not honored for TEF
The /transfers endpoint accepts clientRequestId in the body without complaining (the backend does not reject the field), but does not use it as idempotency. Each request with the same clientRequestId but a different Idempotency-Key produces an independent transaction. Use the Idempotency-Key header exclusively to guarantee idempotency.
Next Steps
- PIX Cash-Out by Key -- for transfers outside Owem.
- HMAC-SHA512 -- signature details.
- API Key -- how to configure permissions.
- Webhooks -- how to subscribe and validate events.