Accounts

Where money lives, and how it's allowed to move.

Penny models money with three kinds of account. Capital Accounts hold real balances. Control Accounts hold no balance of their own; they scope how much can be spent against a Capital Account, and how fast. Linked Accounts are external bank accounts you register as sources of incoming funds. All three share the account_ ID prefix, and every account response carries a type field (capital, control, or linked) that tells you which kind it is.

Capital Accounts

A Capital Account holds an actual settled balance: this is your real money. It reports two balance fields: available_balance (what’s free to move right now) and total_balance (including funds held against pending activity). Capital Accounts can be:

  • Nested: every Capital Account you create sits under a parent_account_id, so one funding source can be split across departments or products. Your business’s top-level Capital Account has root: true.
  • Auto-topped-up: configure auto_top_up with a source_account_id (another Capital Account), a threshold_amount, and a top_up_amount, and Penny moves top_up_amount from the source whenever the balance drops below the threshold.
  • Alerted: set low_balance_notification.threshold_amount (via PATCH /accounts/capital/{account_id}/low_balance_alert) and receive an account.low_balance_alert webhook when the balance drops below it.
A capital account
{
"account_id": "account_019372e3-b8dc-7de2-80b6-b8dcde20b6c9",
"type": "capital",
"version": 3,
"version_time": "2026-08-14T16:05:12Z",
"alias": "operations-account",
"description": "Primary operating account for day-to-day spend.",
"program_id": "program_019377de-7f88-7f02-894f-7f88f0294fcb",
"business_id": "business_01937b95-484e-75af-8c68-484e5afc68d3",
"business_entity_id": "business_entity_01937c9d-3a1e-72b0-8881-3a1e2b088159",
"status": "active",
"active": true,
"currency": "USD",
"parent_account_id": "account_019372e1-4c2a-7b10-9d3e-4c2a7b109d3e",
"root": false,
"available_balance": "48200.00",
"total_balance": "50200.00",
"auto_top_up": {
"source_account_id": "account_019372e1-4c2a-7b10-9d3e-4c2a7b109d3e",
"source_account_type": "capital",
"threshold_amount": "10000.00",
"top_up_amount": "25000.00"
},
"low_balance_notification": { "threshold_amount": "5000.00" }
}

Keys whose value is null are omitted from responses. This account has no spend_control_id, so the key is absent.

Control Accounts

A Control Account holds no balance. It is tied to one Capital Account (capital_account_id), and spend through it is drawn from that Capital Account’s balance. It exists to answer one question: how much is allowed to move through here, and how fast? Control Accounts can nest under a parent_account_id, and each one can carry its own Spend Control via spend_control_id.

This is the layer you attach to cards, cardholders, or business units when you want a shared limit across many of them. A card and its cardholder can point at the same Control Account and share one budget, backed by one Capital Account’s balance.

When you create a Control Account (POST /accounts/control/), the request names the Capital Account as source_capital_account_id and an optional parent as parent_control_account_id. The response reports them as capital_account_id and parent_account_id:

A control account
{
"account_id": "account_019372e3-9f4a-7c1b-9a3d-9f4a7c1b9a3d",
"type": "control",
"version": 1,
"version_time": "2026-08-14T16:10:44Z",
"alias": "card-program-control",
"program_id": "program_019377de-7f88-7f02-894f-7f88f0294fcb",
"business_id": "business_01937b95-484e-75af-8c68-484e5afc68d3",
"business_entity_id": "business_entity_01937c9d-3a1e-72b0-8881-3a1e2b088159",
"status": "active",
"active": true,
"currency": "USD",
"capital_account_id": "account_019372e3-b8dc-7de2-80b6-b8dcde20b6c9",
"spend_control_id": "spend_control_019379d9-a170-796b-867a-a17096b67a21"
}

This Control Account sits directly under its Capital Account, so parent_account_id is null and omitted.

Reading balances and history

Every Capital and Control Account exposes its own ledger and transaction history:

GET /accounts/capital/{account_id}/ledger
GET /accounts/capital/{account_id}/transactions
GET /accounts/control/{account_id}/ledger
GET /accounts/control/{account_id}/transactions

See Transactions for what a transaction represents, and Reading Transactions & Ledger Entries for a worked example.

Moving money between accounts

POST /accounts/capital/transfer moves funds from one Capital Account to another.

FieldRequiredDescription
from_account_idYesThe Capital Account the funds leave.
to_account_idYesThe Capital Account the funds arrive in.
amountYesA positive decimal string, for example "250.00".
currencyNo"USD" (the default and the only supported currency). Must match both accounts.
descriptionNoUp to 256 characters describing the transfer.
client_referenceNoYour own reference, copied onto both resulting transactions.

Send an Idempotency-Key header with every transfer. It is optional, but strongly advised: if a request times out, retrying with the same key returns the original result instead of moving the money twice. See Retries & Idempotency.

cURL
curl -X POST https://sandbox.api.thepennyinc.com/accounts/capital/transfer \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 5f0c7a52-3b8e-4f7a-9d61-2c4e8b1a7f30" \
-d '{
"from_account_id": "account_019372e3-b8dc-7de2-80b6-b8dcde20b6c9",
"to_account_id": "account_019372e4-0a61-7e5c-8f2b-0a617e5c8f2b",
"amount": "250.00",
"currency": "USD",
"description": "Monthly marketing budget",
"client_reference": "budget-2026-09-marketing"
}'

The 201 Created response contains both sides of the transfer: source_transaction (the outbound transaction on from_account_id) and destination_transaction (the inbound transaction on to_account_id). Both are transfer-type Transactions with an internal channel and a transfer counterparty naming the other account. Abridged response:

201 Created
{
"source_transaction": {
"transaction_id": "transaction_01937a80-1c4d-7a2e-9b5f-1c4d7a2e9b5f",
"account_id": "account_019372e3-b8dc-7de2-80b6-b8dcde20b6c9",
"type": "transfer",
"direction": "outbound",
"base_amount": "250.00",
"base_amount_currency": "USD",
"client_reference": "budget-2026-09-marketing",
"channel": { "type": "internal" },
"counterparty": { "type": "transfer", "account_id": "account_019372e4-0a61-7e5c-8f2b-0a617e5c8f2b" }
// …
},
"destination_transaction": {
"transaction_id": "transaction_01937a80-1c4e-7b3f-8a6c-1c4e7b3f8a6c",
"account_id": "account_019372e4-0a61-7e5c-8f2b-0a617e5c8f2b",
"type": "transfer",
"direction": "inbound",
"base_amount": "250.00",
"base_amount_currency": "USD",
"client_reference": "budget-2026-09-marketing",
"channel": { "type": "internal" },
"counterparty": { "type": "transfer", "account_id": "account_019372e3-b8dc-7de2-80b6-b8dcde20b6c9" }
// …
}
}

Account status and lifecycle

Every Capital and Control Account carries a status:

StatusMeaning
activeThe account can be used normally.
deactivatedThe account is temporarily disabled and can be reactivated.
suspendedThe account has been suspended.
deletingDeletion has been requested and is in progress; the account is no longer usable.
deletedThe account has been permanently deleted.
PATCH /accounts/capital/{account_id}/activate
PATCH /accounts/capital/{account_id}/deactivate
DELETE /accounts/capital/{account_id}

The same three operations exist under /accounts/control/{account_id} for Control Accounts. Linked Accounts have their own statuses; see Linking an Account.

Activating and deactivating an account both apply immediately and return the updated account. Deleting an account is asynchronous: the request returns 202 Accepted with the account (item) and the Task that completes the deletion (task). Deleting an account whose deletion has already started is rejected. Abridged response:

202 Accepted from DELETE /accounts/capital/{account_id}
{
"item": {
"account_id": "account_019372e3-b8dc-7de2-80b6-b8dcde20b6c9",
"type": "capital",
"status": "deleting"
// … the rest of the capital account
},
"task": {
"task_id": "task_0193749d-6d72-7481-83a4-6d724813a48a",
"version": 1,
"version_time": "2026-09-02T14:21:07Z",
"business_id": "business_01937b95-484e-75af-8c68-484e5afc68d3",
"status": "received"
}
}

A task’s status is one of received, scheduled, pending, pending_subtasks, running, completed, failed, or cancelled. The task also carries start_time once it starts running and finished_time once it finishes. To check on the deletion, poll GET /tasks/{task_id}, or re-fetch the account with GET /accounts/capital/{account_id} (or GET /accounts/control/{account_id}) and check its status.