Skip to content

TEF -- Transfer Between Owem Accounts

Transfer between two Owem accounts (TEF). No BACEN routing, no fee, immediate settlement via TigerBeetle.

Endpoint

POST /api/external/transfers

Difference vs PIX Cash-Out

AspectTEF (/transfers)PIX Cash-Out (/pix/cash-out)
Destination ISPBAlways 37839059 (Owem)Any PSP
endToEndId in responseDoes not existE{ISPB}{YYYYMMDDHHmm}{entropy}
feeAmountAlways 0Fee may be charged
SettlementImmediate via TigerBeetleAsynchronous via SPI/BACEN
Webhook familytef.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

HeaderTypeRequiredDescription
AuthorizationStringYesApiKey {client_id}:{client_secret}
Content-TypeStringYesapplication/json
hmacStringYesHMAC-SHA512 signature of the body (hex) -- see HMAC-SHA512
Idempotency-KeyStringNoUnique 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

FieldTypeRequiredDescription
amountIntegerYesAmount 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.
descriptionStringNoTransfer description (max 140 characters)
externalIdStringNoYour 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

FieldTypeRequiredDescription
destinationKeyStringYesPIX key of the destination Owem account
destinationKeyTypeStringYesCPF | CNPJ | EMAIL | PHONE | EVP

Mode B -- destination by Owem agency + account

FieldTypeRequiredDescription
destinationAgencyStringYes4 digits (e.g. 0001)
destinationAccountNumberStringYesDestination 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)

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

Example (Mode B -- by agency+account)

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": "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):

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

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.

FieldTypeDescription
workedBooleantrue indicates the request was accepted
finalBooleantrue when the transaction has reached a terminal state (always true on settled TEF)
transactionIdStringUnique transaction identifier (prefix TEF)
externalIdStringYour identifier, returned as sent. null if not provided or discarded
amountIntegerTransfer amount in base units (1 BRL = 10000). To convert to BRL, divide by 10000.
feeAmountIntegerAlways 0 (internal TEF has no fee)
netAmountIntegerEquals amount (no fee)
channelStringAlways "tef"
statusString"settled" on immediate settlement
detailStringDescriptive message

Error Codes

Validation errors (HTTP 422)

Response shape: {"status": "failed", "errors": [{"code": "<code>", "params": {...}}]}

CodeDescription
route_via_pix_cashoutDestination is not an Owem customer. Use POST /api/external/pix/cash-out.
destination_requiredNo destination mode provided (destinationKey+destinationKeyType or destinationAgency+destinationAccountNumber).
destination_ambiguousSent destinationKey AND destinationAgency in the same request.
destination_not_foundDestination key/account does not exist or is inactive at Owem. params includes account_number and agency.
self_transferSource account equals destination account. params.account_id carries the ID.
pix_key_ambiguous11-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": {...}}]}

CodeDescription
insufficient_balanceAvailable balance less than amount.
pix_out_transaction_limit_exceededAmount exceeds the per-transaction limit configured for the account.

Input errors (HTTP 400)

Response shape: {"errors": {"bad_request": "<message>"}}

MessageCause
invalid or missing amountamount missing, zero, negative, or non-integer.

Authentication / authorization errors

HTTPShapeDescription
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).

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

json
{
  "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).

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

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

Owem Pay Instituição de Pagamento — ISPB 37839059