Tutorial: Emit a payroll and reconcile it

This tutorial walks through paying a list of suppliers in a single bank-payroll batch and then reconciling each payment against the corresponding bank transaction. It assumes you have already finished the Getting started quickstart and have an API_KEY and a COMPANY_ID.

The flow has 5 steps:

  1. Pick a source bank account.
  2. Make sure every recipient is enrolled at the bank.
  3. Create the payroll with all transactions.
  4. Pre-authorize and authorize the payroll.
  5. Poll for status until each transaction reconciles.

Total wall time on a real bank: typically 30–90 seconds. Most of that is the bank's processing window.

1. Pick the source bank_account

curl 'https://api.cardda.com/v1/banking/bank_accounts' \
  -H "Authorization: Bearer $API_KEY" \
  -H "company-id: $COMPANY_ID"

Pick the id of the account you want to pay from. We'll call it BANK_ACCOUNT_ID. Cache it on your side — it almost never changes.

2. Make sure every recipient is enrolled

A "recipient" is an external bank account where money will land. To pay them, both these conditions must hold:

  • The recipient exists in Cardda (POST /v1/banking/bank_recipients, see Tutorial: Create and manage recipients).
  • The recipient is enrolled at your bank (POST /v1/banking/bank_recipients/{id}/enroll).

Skip the second step if your bank account is configured for "open" payments (no enrollment required).

3. Create the payroll

A payroll is a container for many transactions that the bank processes as a single batch.

curl -X POST 'https://api.cardda.com/v1/banking/bank_payrolls' \
  -H "Authorization: Bearer $API_KEY" \
  -H "company-id: $COMPANY_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "bank_account_id": "BANK_ACCOUNT_ID",
    "name": "April vendor batch",
    "scheduled_for": "2026-04-30T15:00:00Z",
    "bank_transactions": [
      {
        "recipient_id": "11111111-1111-1111-1111-111111111111",
        "amount": 1500000,
        "currency": "CLP",
        "description": "April invoice 1023"
      },
      {
        "recipient_id": "22222222-2222-2222-2222-222222222222",
        "amount": 280000,
        "currency": "CLP",
        "description": "April invoice 1024"
      }
    ]
  }'
{
  "id": "payroll-uuid",
  "status": "draft",
  "bank_transactions": [
    { "id": "tx-1-uuid", "status": "draft", ... },
    { "id": "tx-2-uuid", "status": "draft", ... }
  ]
}

4. Validate, pre-authorize, authorize

# (a) Cardda validates each recipient exists and is enrolled
curl -X POST 'https://api.cardda.com/v1/banking/bank_payrolls/PAYROLL_ID/validate' \
  -H "Authorization: Bearer $API_KEY" \
  -H "company-id: $COMPANY_ID"

# (b) Cardda forwards the batch to the bank as "pending authorization"
curl -X POST 'https://api.cardda.com/v1/banking/bank_payrolls/PAYROLL_ID/preauthorize' \
  -H "Authorization: Bearer $API_KEY" \
  -H "company-id: $COMPANY_ID"

# (c) The user submits the bank token (TOTP / coordinates / signature)
curl -X POST 'https://api.cardda.com/v1/banking/bank_payrolls/PAYROLL_ID/authorize' \
  -H "Authorization: Bearer $API_KEY" \
  -H "company-id: $COMPANY_ID" \
  -H "Content-Type: application/json" \
  -d '{ "bank_key_token": "123456" }'

After step (c) the payroll status moves to processing, then authorized once the bank accepts it.

5. Reconcile each bank transaction

The payroll's status is the aggregate. Individual bank_transactions can succeed or fail independently.

curl 'https://api.cardda.com/v1/banking/bank_transactions?payroll_id=PAYROLL_ID' \
  -H "Authorization: Bearer $API_KEY" \
  -H "company-id: $COMPANY_ID"

Each item has a status:

StatusMeaningNext step
draftCreated, not yet sentWait for the payroll lifecycle.
pendingAt the bank, awaiting authWait.
authorizedSent and accepted by the bankWait for the bank statement to confirm the funds movement.
confirmedConfirmed in the bank statement✅ Reconciled. Pull the receipt.
failedBank rejected the transactionCheck failure_reason. Recreate manually if needed.
rejectedCardda blocked the transaction (e.g. recipient not enrolled)Fix the underlying issue and retry.

Pull the receipt URL once confirmed:

curl 'https://api.cardda.com/v1/banking/bank_transactions/TX_ID/receipt/url' \
  -H "Authorization: Bearer $API_KEY" \
  -H "company-id: $COMPANY_ID"

Returns a presigned S3 URL valid for 5 minutes:

{ "url": "https://cardda-api.s3.amazonaws.com/.../receipt.pdf?..." }

End-to-end snippet (Python)

import time, requests

api = "https://api.cardda.com/v1"
H = {"Authorization": f"Bearer {API_KEY}", "company-id": COMPANY_ID}

def post(path, body=None):
    r = requests.post(api + path, headers={**H, "Content-Type": "application/json"}, json=body, timeout=30)
    r.raise_for_status()
    return r.json()

def get(path, **params):
    r = requests.get(api + path, headers=H, params=params, timeout=30)
    r.raise_for_status()
    return r.json()

# 3. Create payroll
payroll = post("/banking/bank_payrolls", {
    "bank_account_id": BANK_ACCOUNT_ID,
    "name": "April vendor batch",
    "bank_transactions": [
        {"recipient_id": "...", "amount": 1500000, "currency": "CLP", "description": "Inv 1023"},
        {"recipient_id": "...", "amount": 280000,  "currency": "CLP", "description": "Inv 1024"},
    ],
})

# 4. Validate + preauthorize + authorize
post(f"/banking/bank_payrolls/{payroll['id']}/validate")
post(f"/banking/bank_payrolls/{payroll['id']}/preauthorize")
post(f"/banking/bank_payrolls/{payroll['id']}/authorize", {"bank_key_token": prompt_user()})

# 5. Poll for reconciliation (up to 5 minutes)
deadline = time.time() + 300
while time.time() < deadline:
    txs = get("/banking/bank_transactions", payroll_id=payroll["id"])
    statuses = {tx["status"] for tx in txs}
    if statuses <= {"confirmed", "failed", "rejected"}:
        break
    time.sleep(10)

print({tx["id"]: tx["status"] for tx in txs})

Common pitfalls

  • Recipient not enrolled. The bank rejects the line item with rejected. Look at failure_reason and call /bank_recipients/{id}/enroll.
  • Insufficient balance. The bank usually rejects the entire payroll (status failed) rather than partials.
  • Wrong currency. All transactions in a single payroll must share the same currency.
  • Re-authorizing a confirmed payroll. No-op; idempotent.

Related