Managing purchase orders

Follow committed spend, create purchase orders from your ERP or procurement tool, track their billing, and close or cancel them.

Purchase orders commit spend with a supplier before the invoices arrive. Through the API you can:

  • follow what is committed — open purchase orders, their amounts, and how much has been invoiced so far;
  • create purchase orders from your ERP or procurement tool, so buyers don't enter them twice;
  • track billing — the invoices attached to each purchase order and what remains to be billed;
  • close or cancel a purchase order when it is done or no longer needed.
📘

Experimental

Purchase order endpoints are experimental: they may still change. Request access from your CSM or at [email protected] — see Getting access.

Before you start

  • Scopes: experimental:purchase-order:read to read purchase orders and their documents, experimental:purchase-order:write to create, close and cancel them.
  • To create purchase orders, the Spendesk IDs they refer to: the requester (userId, from Get Users), the supplier (supplierId, from Get Suppliers), the cost center (costCenterId, from Get Cost Centers), and the custom fields your company requires.
  • With an organisation-level key, send the X-Company-Id header on every call — see Organisation-level access.

How a purchase order looks

FieldMeaning
idSpendesk ID, used in every other call
purchaseOrderNumberThe number people see in Spendesk (17 for PO-17)
statusopen — spend can still be billed against it; closed — done; cancelled — no longer valid
amount, netAmountThe committed amount, with and without VAT
billedAmountWhat invoices attached to it add up to so far
deliveredAmountWhat has been delivered, when delivery notes are expected
billingStatusWhere billing stands — for example waitingForBilling, or lateInvoice when the period has ended without an invoice
deliveryStatusnoDeliveryExpected unless the purchase order expects delivery notes
startDate, endDateThe period the purchase order covers
supplierId, requesterId, costCenterIdWho it is with, who asked for it, and where it is booked
invoices[]The invoices attached to it, each with its invoiceNumber, amount and status: pending-approval, in-review, to-schedule, paid
items[]Line items (name, quantity, unit price, VAT rate), returned when you pass withItems=true

Amounts are objects: { "amount": 12000, "currency": "EUR", "precision": 2 } is 120.00 EUR — divide amount by 10 to the power of precision.

1. List purchase orders

Get Purchase Orders returns 30 purchase orders per page, filtered by status, supplierIds, creation dates (createdFrom, createdTo), or period (startDateFrom, startDateTo, endDateFrom, endDateTo) — dates as YYYY-MM-DD:

curl -G https://public-api.spendesk.com/v1/purchase-orders \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  --data-urlencode "status=open" \
  --data-urlencode "pageSize=30" \
  --data-urlencode "page=1"

Page through the results with page until you have meta.pagination.total purchase orders — see Pagination. Add withItems=true for the line items.

2. Follow billing

For one purchase order, Get a purchase order by ID returns the same fields. What remains to be billed is amount.amount − billedAmount.amount, and invoices[] lists the invoices attached so far with their status.

To work from the invoices instead, List invoices takes a purchaseOrderId filter and returns each invoice's purchaseOrders[] — see List the invoices due this week for how the invoice endpoints work.

3. Create a purchase order

Create a purchase order takes the requester, the supplier, the cost center, the amounts and the period:

curl -X POST https://public-api.spendesk.com/v1/purchase-orders \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "userId": "USER_ID",
    "supplierId": "SUPPLIER_ID",
    "costCenterId": "COST_CENTER_ID",
    "amount": { "amount": 12000, "currency": "EUR", "precision": 2 },
    "netAmount": { "amount": 10000, "currency": "EUR", "precision": 2 },
    "description": "Office chairs — Q4",
    "startDate": "2026-10-01T00:00:00.000Z",
    "endDate": "2026-10-31T23:59:59.999Z",
    "customFieldAssociations": [],
    "externalPONumber": "ERP-PO-4711"
  }'
{
  "data": {
    "purchaseOrderId": "8o5hx11zwqswte",
    "purchaseOrderNumber": "PO-17",
    "alreadyExisted": false
  }
}

The purchase order is created open, with billingStatus waitingForBilling. Keep purchaseOrderId: every other call uses it.

  • Dates take a date and time (2026-10-01T00:00:00.000Z); a date alone is refused with 400.
  • Supplier: pass supplierId for an existing supplier, or supplierName instead.
  • Custom fields: customFieldAssociations is required — an empty list when your company has no required custom field; otherwise one entry per field, with its customFieldId and a customFieldValueId or free value.
  • Optional: items (line items), analyticalSplits (split the amount across cost centers or custom field values by percentage), teamId, and deliveryNotesExpected when you expect delivery notes for it.
  • No duplicate check: sending the same request twice creates two purchase orders, even with the same externalPONumber. Before retrying a request that timed out, look for the purchase order in Get Purchase Orders. externalPONumber is not returned when you read purchase orders: keep the mapping between your number and purchaseOrderId on your side.

4. Close or cancel

  • Close a purchase order that is done — invoiced and paid — with Close a purchase order. A closed purchase order can no longer be modified.
  • Cancel one that is no longer needed, before any invoice is attached, with Cancel a purchase order.

Both calls take the purchase order's ID and an empty JSON body — without a body, the request is refused with 406:

curl -X POST https://public-api.spendesk.com/v1/purchase-orders/8o5hx11zwqswte/cancel \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{}'
{ "data": { "outcome": "cancelled" } }

When the change is not possible, outcome is notCancelled or notClosed, and reason says why:

CallMain reasonsWhat to do
CancelattachedInvoicesExist, hasExpensesInvoices or expenses are already attached: close it instead once they are paid
ClosenoAttachedInvoicesNothing was billed: cancel it instead
CloseunpaidInvoicesWait until the attached invoices are paid
BothstatusNotOpenedIt is already closed or cancelled
BothpurchaseOrderNotFound, forbiddenCheck the ID, and the company with an organisation-level key

Only close or cancel an open purchase order: read its status first. On a purchase order that is already cancelled, both calls currently fail with 500 rather than returning statusNotOpened.

5. Download the document

Get Purchase Order Document returns a short-lived url to the purchase order's PDF and its mimeType. Download the file right away and store it on your side: the link expires. The call returns 404 when no document has been generated for the purchase order.

With an AI assistant

Connected through the Spendesk MCP server, an assistant can list purchase orders and, with the matching permission, create, cancel or close them:

Which purchase orders are still open, for which supplier, and for what amount?

Good to know

  • Amounts are always in the smallest unit of the currency, with their precision.
  • Data may be cached for a short period: a purchase order you just created or changed may take a moment to show in lists.
  • Test on the demo environment first, https://public-api.demo.spendesk.com: creating, closing and cancelling cannot be undone.

Last checked against the Spendesk demo environment on 29 September 2026.