Expense Management: Capture, Approve, Pay, Account

If you're shipping expense features to your customers, the hard part isn't the form where someone types in a receipt. It's everything after: did the cost land in the right expense account, does accounts payable reflect what's actually owed, and can an accountant trace each payment back to the bill it settled? This guide builds that whole path over the API — and, as always in this series, keeps going until you can read the double-entry records back from the ledger.

There are two distinct flows here, and your product probably needs both:

  • Direct expense — money already left the building (a card swipe, a cash purchase). One call, books update immediately.
  • Vendor bill — an obligation that gets approved first and paid later. Two postings, with an approval gate in between.

What you need:

  • Sandbox client credentials (client_id / client_secret) — self-serve signup, no billing, no sales call
  • A company and at least one vendor record (this guide follows on from Build Embedded Accounting in 20 Minutes — a vendor is created the same way as a customer, against your vendor endpoint, and you'll use its people_id below)
  • curl or any HTTP client
Shell
export ACCESS_TOKEN="<from the credential exchange>"
export BASE="https://api-sandbox.paprel.com"
export AUTH="authorization: Bearer $ACCESS_TOKEN"
export CT="content-type: application/json"

First time? The five-minute setup — token exchange and request signing — is on the Authentication page. The curl examples below show the auth header only; write requests also require an x-data-signature header, which the SDK computes for you.

All paths are relative to the sandbox API base URL. Exact request and response schemas are in the API reference.


Step 1 — Find the expense accounts

Before recording anything, look at where costs can land. The company's chart of accounts already has an expense branch; there's a dedicated read for exactly this purpose:

Shell
curl "$BASE/v1/accounting/expense-accounts" -H "$AUTH"

Endpoint reference: GET /v1/accounting/expense-accounts

This returns all accounts categorized as expenses — operating expenses, cost of goods sold, other expenses. Pick (or create, via the accounts endpoint from the 20-minute guide) the account each cost category should post to. In an embedded setup this is the mapping screen you build once: your customer's "Software & Subscriptions" category points at an expense_account_id, and every capture after that is automatic.

Step 2 — Capture a direct expense

The team lead pays for a SaaS subscription on the company card. The money is already gone, so record it as a direct expense — note paid_account_id is set, naming the bank/cash account the money left:

Shell
curl -X POST $BASE/v1/expenses \
  -H "$AUTH" -H "$CT" \
  -d '{
    "expense_date": "2026-06-12",
    "currency": "USD",
    "vendor_id": "<vendor people_id>",
    "paid_account_id": "<bank/cash account id>",
    "is_itemize": true,
    "is_billable": false,
    "reference_number": "CARD-20260612-014",
    "note": "Design tooling, June",
    "items": [
      {
        "expense_account_id": "<expense account id from step 1>",
        "amount": "89.00",
        "description": "Figma seat, monthly"
      }
    ]
  }'

Endpoint reference: POST /v1/expenses

Each line item carries its own expense_account_id, so one receipt can split across categories — half software, half training — and each slice lands in its own account. Attach the receipt image via the attachment field if you have one; it stays linked to the expense for audit.

Behind the scenes: because paid_account_id is set, this posts immediately. No approval step, no payable — the cash already moved, the books just catch up:

AccountDebitCredit
Software & Subscriptions89.00
Cash · Operating89.00

Expense up, cash down, balanced at write time. This is the right shape for card transactions and reimbursable receipts where the spend is a fact, not a request.

Step 3 — Raise a vendor bill (the approve-then-pay flow)

Now the other case: an invoice arrives from a vendor — say a contractor's monthly bill — and you don't want it paid until someone signs off. That's a vendor bill. Bills mirror invoices structurally (same document model, opposite direction): people_id is the vendor, and the totals post to payables instead of receivables.

Shell
curl -X POST $BASE/v1/bills \
  -H "$AUTH" -H "$CT" \
  -d '{
    "people_id": "<vendor people_id>",
    "currency": "USD",
    "document_issue_date": "2026-06-12",
    "document_due_date": "2026-07-12",
    "items": [
      {
        "item_name": "Contract engineering — June",
        "item_quantity": "1",
        "item_cost": "5200.00",
        "item_description": "160 hours @ agreed rate"
      }
    ]
  }'

Endpoint reference: POST /v1/bills

The response includes the bill's anchor_id — the stable identifier that survives edits. Use it for the payment in step 4. (Bills are versioned the same way invoices are; the anchor is what stays constant across revisions.)

Behind the scenes: posting the bill recognizes the cost now and the obligation alongside it:

AccountDebitCredit
Contract Services Expense5,200.00
Accounts Payable5,200.00

No cash has moved. That's the accrual distinction your customers' accountants care about: the expense belongs to June, even if the payment happens in July. Accounts payable now shows exactly what's owed to this vendor — a sub-ledger fact you can query, not a spreadsheet you maintain.

The approval gate: bill statuses are configurable per company via the bill status settings — define the states your workflow needs (received → reviewed → approved, or whatever matches your product) and transition bills through them. Each status change lands in the document's activity history, so "who approved this and when" is always answerable. Your platform's approval UI is a thin layer over these transitions: hold payment until the bill reaches the approved state, then proceed to step 4. The status configuration endpoints are documented in the API reference.

Step 4 — Pay the bill

Approval granted. Settle it:

Shell
curl -X POST $BASE/v1/bills/<anchor_id>/payment \
  -H "$AUTH" -H "$CT" \
  -d '{
    "payment_amount": "5200.00",
    "payment_date": "2026-07-01",
    "payment_mode": "…",
    "deposit_account": "<bank account id>",
    "reference_number": "WIRE-20260701-007"
  }'

Endpoint reference: POST /v1/bills/:anchor_id/payment

Behind the scenes: the second posting clears the obligation:

AccountDebitCredit
Accounts Payable5,200.00
Cash · Operating5,200.00

Payables for this vendor drop by 5,200, cash drops, and the expense is untouched — it was recognized in step 3, in the right period. The bill flips to paid and the payment links to it permanently via the anchor.

Retried the request because of a timeout? The write path is idempotent — resubmitting returns the original result instead of paying the bill twice.

Step 5 — Read the journals back

Prove it. Ask the ledger what happened:

Shell
curl "$BASE/v1/accounting/journals" -H "$AUTH"

Endpoint reference: GET /v1/accounting/journals

You'll recognize all three postings from this guide — the direct expense from step 2, the bill from step 3, the payment from step 4 — each as a balanced journal against accounts in the company's chart. The journals endpoint supports filtering by date range, account, and reference, so "show me everything that touched Accounts Payable this month" is one query.

This is the audit trail your customers' accountants will actually check: every expense traceable to an account, every payable traceable to a bill, every payment traceable to what it settled. None of it was a second system you reconciled — the journals are the books, and the P&L and balance sheet are derived from these rows live.

A note on billable expenses

If a cost is incurred on behalf of a client — travel for a customer project, a stock-photo license for their campaign — set is_billable: true on the expense to flag it for pass-through into that client's billing, so the cost can be recovered on a future invoice instead of silently eaten as overhead.


What you just built

Two capture paths (immediate and approve-then-pay), a configurable approval gate with a full audit history, idempotent payment recording, and books that updated themselves correctly at every step — expense recognition in the right period, payables that always match outstanding bills, and journals you can hand to an auditor.

Where to go next:

Evaluate Paprel

Build in sandbox, launch with a production trial

Use sandbox for developer testing with no billing. When you are ready for real workflows, start production on a monthly plan with a 14-day free trial.

API-First Delivery
Audit-Ready Controls
Sandbox And Guided Rollout
We use cookies to improve your experience. Manage preferences or accept all.