Modules

Purchases

Purchase orders with approvals and receipts, vendor bills and payments.

Overview

The Purchases module covers what you buy: a purchase order (po_...) goes through approval, is sent to the vendor and received (adding stock when Inventory is on). A bill (pinv_...) is the vendor invoice you have to pay, created from an order or manually, with its payments (accounts payable).

Requires the Purchases module. An owner or admin turns it on in Settings > Modules. While it is off, every endpoint on this page returns 403 with type permission_error and code module_disabled.

Every write is recorded with the user who owns the API key as the author. That user is also the one checked as approver when you approve or reject an order.

Purchase Order Lifecycle

ActionFromTo
submitdraft, rejected (with at least one line)pending_approval, or approved when approvals are off
approve / rejectpending_approvalapproved / rejected
mark_sentapprovedsent (no email is sent)
receiveapproved, sent, partially_receivedpartially_received or received
cancelAny status before goods are received or bills existcancelled

The Purchase Order Object

Abbreviated. See the API reference for every field
1{
2 "id": "po_4hd",
3 "object": "purchase_order",
4 "po_number": "PO-000007",
5 "status": "approved",
6 "vendor_id": "ven_12",
7 "vendor_name": "Acme Supplies",
8 "issue_date": "2026-10-11",
9 "expected_date": "2026-10-20",
10 "warehouse_id": "wh_7k2",
11 "currency": "USD",
12 "payment_terms_days": 30,
13 "tax_rate": 18,
14 "subtotal": 100,
15 "discount_amount": 0,
16 "tax_amount": 18,
17 "total": 118,
18 "purchase_type": "national",
19 "approved_at": "2026-10-11T15:20:00.000Z",
20 "lines": [
21 {
22 "id": "poi_1",
23 "object": "purchase_order_line",
24 "item_id": "itm_9f2c1a",
25 "item_name": "USB-C cable 2m",
26 "quantity": 25,
27 "unit": "unit",
28 "unit_cost": 4,
29 "subtotal": 100,
30 "total": 100,
31 "received_quantity": 0
32 }
33 ],
34 "receipts": [],
35 "created_at": "2026-10-11T15:00:00.000Z",
36 "updated_at": "2026-10-11T15:20:00.000Z"
37}

Endpoints

MethodPathPermissionDescription
GET/v1/purchase-ordersreadList orders (status, vendor_id, from, to)
POST/v1/purchase-orderswriteCreate a draft (submit: true to submit it)
GET/v1/purchase-orders/{id}readRetrieve an order with lines and receipts
POST/v1/purchase-orders/{id}writeUpdate (lines replaces all lines)
DELETE/v1/purchase-orders/{id}adminDelete a draft
POST/v1/purchase-orders/{id}/submitwriteSubmit for approval
POST/v1/purchase-orders/{id}/approvewriteApprove (optional comment)
POST/v1/purchase-orders/{id}/rejectwriteReject (comment required)
POST/v1/purchase-orders/{id}/mark_sentwriteMark as sent to the vendor
POST/v1/purchase-orders/{id}/cancelwriteCancel (optional reason)
POST/v1/purchase-orders/{id}/receivewriteReceive goods
GET/v1/billsreadList bills (status, vendor_id, purchase_order_id, overdue, from, to)
POST/v1/billswriteCreate a draft bill (register: true to register it)
GET/v1/bills/{id}readRetrieve a bill with lines and payments
POST/v1/bills/{id}writeUpdate a bill
DELETE/v1/bills/{id}adminDelete a draft bill
POST/v1/bills/{id}/registerwriteRegister (draft to open)
POST/v1/bills/{id}/record_paymentwriteRecord a payment to the vendor
POST/v1/bills/{id}/cancelwriteCancel a bill without payments

Create a Purchase Order

1curl -X POST "https://api.corebill.io/v1/purchase-orders" \
2 -H "Authorization: Bearer sk_live_your_api_key" \
3 -H "Content-Type: application/json" \
4 -H "Idempotency-Key: po-acme-2026-10-11" \
5 -d '{
6 "vendor_id": "ven_12",
7 "expected_date": "2026-10-20",
8 "warehouse_id": "wh_7k2",
9 "tax_id": "tax_1",
10 "lines": [
11 { "item_id": "itm_9f2c1a", "quantity": 25, "unit_cost": 4 }
12 ],
13 "submit": true
14 }'

Only vendor_id is required. Currency and payment terms default to the vendor profile (otherwise the company currency). Each line needs quantity and unit_cost, plus item_id or item_name (up to 200 lines). Lines are validated before creating; if a later step fails (for example submit), the draft is deleted and you get the error. Emits purchase_order.created, and purchase_order.approved when it is auto-approved.

Approve and Receive

1# Approve (the key owner must be an approver whose limit covers the total)
2curl -X POST "https://api.corebill.io/v1/purchase-orders/po_4hd/approve" \
3 -H "Authorization: Bearer sk_live_your_api_key" \
4 -H "Content-Type: application/json" \
5 -d '{ "comment": "Within budget" }'
6
7# Receive 10 of the 25 units
8curl -X POST "https://api.corebill.io/v1/purchase-orders/po_4hd/receive" \
9 -H "Authorization: Bearer sk_live_your_api_key" \
10 -H "Content-Type: application/json" \
11 -d '{ "lines": [{ "po_item_id": "poi_1", "quantity": 10 }], "notes": "First delivery" }'
  • An approver who is not configured, or whose max_amount is below the total, gets 403 not_allowed
  • receive returns 201 with the new receipt in receipts. A line cannot exceed its pending quantity
  • With Inventory on, received goods enter warehouse_id (or the order warehouse) as a purchase movement. Emits purchase_order.received

The Bill Object

Abbreviated
1{
2 "id": "pinv_8a",
3 "object": "bill",
4 "bill_number": "BILL-000004",
5 "supplier_invoice_number": "F001-123",
6 "status": "partial",
7 "vendor_id": "ven_12",
8 "vendor_name": "Acme Supplies",
9 "purchase_order_id": "po_4hd",
10 "issue_date": "2026-10-12",
11 "due_date": "2026-11-11",
12 "overdue": false,
13 "currency": "USD",
14 "total": 118,
15 "amount_paid": 50,
16 "amount_due": 68,
17 "affects_stock": false,
18 "lines": [{ "id": "piit_1", "object": "bill_line", "item_name": "USB-C cable 2m", "quantity": 25, "unit_cost": 4, "total": 100 }],
19 "payments": [
20 {
21 "id": "ppay_1",
22 "object": "bill_payment",
23 "payment_number": "PAY-000001",
24 "payment_date": "2026-10-13",
25 "amount": 50,
26 "payment_method": "bank_transfer",
27 "reference": "TRX-889"
28 }
29 ],
30 "created_at": "2026-10-12T09:30:00.000Z"
31}

Bill statuses: draft, open, partial, paid and cancelled. overdue is true for open or partial bills past their due_date.

Bill an Order and Pay It

1# Bill from the order (copies the lines not billed yet) and register it
2curl -X POST "https://api.corebill.io/v1/bills" \
3 -H "Authorization: Bearer sk_live_your_api_key" \
4 -H "Content-Type: application/json" \
5 -d '{ "purchase_order_id": "po_4hd", "supplier_invoice_number": "F001-123", "register": true }'
6
7# Pay part of it
8curl -X POST "https://api.corebill.io/v1/bills/pinv_8a/record_payment" \
9 -H "Authorization: Bearer sk_live_your_api_key" \
10 -H "Content-Type: application/json" \
11 -H "Idempotency-Key: pay-pinv_8a-1" \
12 -d '{ "amount": 50, "payment_method": "bank_transfer", "reference": "TRX-889" }'
  • Manual bill: send vendor_id and lines instead of purchase_order_id. With Inventory on it adds stock when registered (affects_stock, needs a warehouse_id)
  • A fully billed order returns 409 resource_conflict
  • Payments need an open or partial bill, up to amount_due. Methods: bank_transfer (default), cash, check, credit_card, debit_card, other
  • With the Cash module on, a cash payment is also recorded as a cash out in the register
  • Cancel a bill only without payments; stock it added is returned
  • Events: bill.registered, bill.payment_recorded, bill.paid, bill.cancelled

Errors

StatusCodeWhen
400resource_missingA referenced id does not exist (param says which: vendor_id, warehouse_id, tax_id, lines...)
400invalid_requestA business rule, e.g. "Only draft or rejected orders can be submitted."
403not_allowedThe key owner cannot approve or reject this order
403module_disabledThe Purchases module is off
404purchase_order_not_found / bill_not_foundUnknown id in this company
409resource_conflictThe order is already fully billed