Tutorial: Create and manage bank recipients
A bank_recipient is an external bank account that you can pay from one of your bank_accounts. Before sending money to anyone you need a recipient, and (usually) that recipient needs to be enrolled at your bank.
Recipients are per source account, not per company.A recipient belongs to one bank account — the one in its
owner_id. The same person
enrolled in two of your accounts is two different recipients, with two different ids.Before transferring, look the recipient up scoped to the account that pays:
GET /v1/banking/bank_recipients?owner_id={sender_id}— not by name alone. A transfer whose
recipient_idbelongs to another account is rejected with
422 "Recipient must belong to same account as transaction".
This tutorial covers:
- Creating a recipient.
- Listing and updating recipients.
- Enrolling a recipient at the bank (and the trigger-code flow when the bank requires SMS confirmation).
- Common queries.
1. Create
curl -X POST 'https://api.cardda.com/v1/banking/bank_recipients' \
-H "Authorization: Bearer $API_KEY" \
-H "company-id: $COMPANY_ID" \
-H "Content-Type: application/json" \
-d '{
"owner_id": "BANK_ACCOUNT_ID",
"name": "Acme Vendor",
"rut": "765432107",
"bank_id": "cl_banco_de_chile",
"account_type": "corriente",
"account_number": "1234567890",
"email": "[email protected]",
"alias": "ACME-MAIN"
}'Important fields:
| Field | Type | Required | Notes |
|---|---|---|---|
owner_id | uuid | yes | The bank account this recipient is enrolled in. It is what makes the recipient usable as recipient_id for transfers whose sender_id is that same account. Omitting it returns 404. |
name | string | yes | Legal name of the recipient. Length and character set depend on the bank of the owner_id account — the one you enrol into — not on the recipient's bank_id. Caps, strictest first: Itaú 30, BCI / BICE / STP 40, Santander / Scotiabank / Cardda wallet 60, Banco de Chile 64; Banco Security, Banco Internacional and BCP set none. Over the cap you get 422 "Validation failed: Name is too long (maximum is N characters)". Two owner banks also restrict the characters: BICE takes unaccented ASCII letters and spaces only — no digits, no punctuation, no accents (422 "Validation failed: Name recipient's names only can contain letters", so Proveedor ABC SpA is fine and Acme 2 S.A. is not) — and BCI takes A-zÀ-ú0-9 - ñÑ & . @ ( ). Write for the tightest account you enrol from and the same name travels everywhere. |
rut | string | yes (CL) | Tax id, digits and an optional uppercase K only — no dots, no hyphen. 765432107, not 76.543.210-7 nor 76543210-7. |
bank_id | string | yes | Bank slug, e.g. cl_banco_de_chile. Get the catalog from GET /v1/banking/banks (no company-id needed; reference data). For a Chilean owner account it must start with cl. |
account_type | enum | yes | Chile: corriente, vista, ahorro. Mexico: clabe. Peru: yape. |
account_number | string | yes | Digits only for Chile, and it cannot start with 0 — strip leading zeros. Length comes from the owner_id account's bank too: BICE 17, BCI 18, 20 elsewhere in Chile; Mexico is an 18-digit CLABE (must be 18 digits, plus a check-digit test) and Peru is exactly 9 digits. |
email | string | no | Where the bank sends payment receipts. Must be a valid address if present, and capped at 40 chars from an Itaú account. |
alias | string | depends | Required at banks without a specific override (BCI, Itaú, Banco Estado, Banco Internacional, Banco Security, …). Optional at Banco de Chile, BICE, Santander, Scotiabank, Cardda wallet, STP and BCP. The cap also comes from the owner_id account's bank, and here too the strictest banks are the ones that require it: Itaú 20, BCI 25, STP 40, BICE 50, Banco de Chile 64, everyone else 60. At BCI the alias takes the same character set as name. |
transition | enum | no | Defaults to enroll (send it to the bank right away). Pass null to keep it as a local draft — usable in payrolls, not for individual transfers. |
Successful response:
{
"id": "11111111-1111-1111-1111-111111111111",
"owner_id": "BANK_ACCOUNT_ID",
"status": "draft",
"transition": "enroll",
"name": "Acme Vendor",
"rut": "765432107",
"bank_id": "cl_banco_de_chile",
"account_type": "corriente",
"account_number": "1234567890",
"created_at": "2026-04-29T18:00:00Z"
}
There is a single state machine, not two.statusmovesdraft → enrolled → preauthorized → approved(plusdeleted), andtransitionnames the operation in flight (enroll,authorize,preauthorize,delete, ornullwhen nothing is running). Only a recipient inapprovedcan receive an individual transfer. There is noenrollment_statusfield.
2. List, update, delete
# The list you actually want before a transfer: recipients of the paying account
curl -G 'https://api.cardda.com/v1/banking/bank_recipients' \
--data-urlencode 'owner_id=BANK_ACCOUNT_ID' \
--data-urlencode 'status=approved' \
-H "Authorization: Bearer $API_KEY" -H "company-id: $COMPANY_ID"
# List with filters — recipients of a specific bank, alphabetically
curl 'https://api.cardda.com/v1/banking/bank_recipients?bank_id=cl_banco_de_chile&_order=asc&_field=name' \
-H "Authorization: Bearer $API_KEY" -H "company-id: $COMPANY_ID"
# Update — regular (non-admin) callers can only change `metadata`
curl -X PATCH 'https://api.cardda.com/v1/banking/bank_recipients/RECIPIENT_ID' \
-H "Authorization: Bearer $API_KEY" -H "company-id: $COMPANY_ID" \
-H "Content-Type: application/json" \
-d '{ "metadata": { "erp_code": "ACME-001" } }'
# Delete — Cardda wallet recipients only (soft-delete: status becomes `deleted`)
curl -X DELETE 'https://api.cardda.com/v1/banking/bank_recipients/RECIPIENT_ID' \
-H "Authorization: Bearer $API_KEY" -H "company-id: $COMPANY_ID"
PATCHaccepts onlymetadatafrom regular API callers —alias,nameand theaccount coordinates are not editable through it. To change them, create a new recipient.
DELETEis currently implemented only for Cardda wallet recipients; on a real-bank
recipient the call fails. Deleting a wallet recipient that still has transfers in flight is
rejected with422 {"code": "recipient/destroy/blocking_transactions"}. A deleted recipient
can no longer receive transfers (422 "Recipient has been deleted").
3. Enroll at the bank
Most banks require you to "enroll" a recipient before sending money. The flow has two shapes — your bank decides which one applies.
The account and the bank key are resolved from the recipient itself (owner_id), so none of the
calls below needs an account id in the body.
3a. Direct enrollment
Most banks accept enrollment with just the bank credentials. Creating the recipient already starts
it (transition: "enroll"); use this endpoint to retry a recipient left in draft (or one that
was deleted):
curl -X POST 'https://api.cardda.com/v1/banking/bank_recipients/RECIPIENT_ID/enroll' \
-H "Authorization: Bearer $API_KEY" -H "company-id: $COMPANY_ID" \
-H "Content-Type: application/json" -d '{}'It enqueues the enrollment and answers 201 with transition: "enroll". If the recipient is not
in draft/deleted with no transition in flight, it answers 422 with the unchanged recipient.
Poll GET /v1/banking/bank_recipients/{id} until status reaches enrolled (or approved at
banks that need no second step, such as Banco Security and Banco Internacional).
Banco de Chile additionally requires token and device_id in the body — omitting either
returns 412 Precondition Failed:
curl -X POST 'https://api.cardda.com/v1/banking/bank_recipients/RECIPIENT_ID/enroll' \
-H "Authorization: Bearer $API_KEY" -H "company-id: $COMPANY_ID" \
-H "Content-Type: application/json" \
-d '{ "token": "03751824", "device_id": "17b05803-ff34-4b17-8850-4d11217a2ae8" }'3b. SMS / coordinate-card flow
Some banks require a one-time challenge before the recipient reaches approved.
trigger_code is only available where the bank supports it — today BCI, and only while the
recipient is in enrolled. Anywhere else it answers 422 with the unchanged recipient.
# 1. Request the code (bank sends SMS / shows challenge). 204 No Content on success.
curl -X POST 'https://api.cardda.com/v1/banking/bank_recipients/RECIPIENT_ID/trigger_code' \
-H "Authorization: Bearer $API_KEY" -H "company-id: $COMPANY_ID" \
-H "Content-Type: application/json" -d '{}'
# 2. The user receives the code; you collect it from your UI
# 3. Submit it — the field is `verification_code` (or `token` for token-based banks).
# You must send at least one of the two, or the call fails with 412.
curl -X POST 'https://api.cardda.com/v1/banking/bank_recipients/RECIPIENT_ID/authorize' \
-H "Authorization: Bearer $API_KEY" -H "company-id: $COMPANY_ID" \
-H "Content-Type: application/json" \
-d '{ "verification_code": "847239" }'If you receive SMS codes via webhook (PLH issuer), see Verification Codes via Webhook — you can fully automate step 2.
4. Useful queries
# Ready-to-pay recipients of one account — the query to run before a transfer
curl -G 'https://api.cardda.com/v1/banking/bank_recipients' \
--data-urlencode 'owner_id=BANK_ACCOUNT_ID' --data-urlencode 'status=approved' ...
# All recipients still not enrolled at the bank
curl -G 'https://api.cardda.com/v1/banking/bank_recipients' \
--data-urlencode 'status=draft' ...
# Search by name — a bare value is an EXACT match, use $regex for partial/case-insensitive
curl -G 'https://api.cardda.com/v1/banking/bank_recipients' \
--data-urlencode 'name={"$regex":"acme"}' ...
# Recipients of any of two banks
curl -G 'https://api.cardda.com/v1/banking/bank_recipients' \
--data-urlencode 'bank_id={"$in":["cl_banco_de_chile","cl_banco_bci"]}' ...See Filters for the full operator reference. There is no $like/$ilike operator —
partial matching is $regex.
Common pitfalls
- Looking the recipient up without the account scope. The single most expensive mistake: the
same person can exist as a recipient in several of your accounts. Always filter by
owner_id={sender_id}before picking arecipient_id. - Duplicate detection. Uniqueness is
(owner_id, rut, bank_id, account_number)— note it is
scoped to the owning account, so the same destination in two accounts is legitimately two
rows. Recreating the same quadruplet in the same account fails with
422 "Owner already has this recipient". - Formatting
rutoraccount_number. Dots and hyphens inrut, or a leading0in a
Chileanaccount_number, are rejected with a422. Strip them before sending. - Missing
alias. At banks that require it, omittingaliasfails with
422 "Alias can't be blank". bank_idmismatch. The recipient bank must be the same as the destination on the transaction. You cannot pay anACMErecipient at Banco de Chile from a Santander source account if the recipient was registered against Itau.
Related
- Tutorial: Emit a payroll and reconcile it
- Verification Codes via Webhook — automate enrollment SMS handling.
- Errors
Updated 6 days ago
