Approvals

The Procurement App's reference: the submit call that decides a cart, the approval rules that route it, the purchase requests and pending approvals it creates, direct orders, budget releases, the reconciliation sweep and the vocabularies that name the values.

The Procurement App decides what happens to a B2B cart. A direct order is placed at once. A purchase request waits for one or more approvers, each named by an approval rule. A cart that a rule blocks is refused and nothing is created. The App asks the Cost centers App whether a budget covers the spend, holds that budget while a request waits, and spends it when an order exists. It keeps no budget of its own.

For the step-by-step flow, see Set up purchase approvals and cost-center budgets. For the budgets the App draws on, see Cost centers. For the other Procurement Apps, see Procurement.

Base path: https://api.revenexx.com/v1/procurement. The calls take the usual X-Revenexx-Tenant and X-Revenexx-Api-Key headers.

Scopes

procurement.* covers every call on this page, writes and decisions included. The checkout server that calls submit also needs cost_centers.* and customers.* (see which scope covers which app).

Submitting a cart

OperationPathUse
SubmitPOST /v1/procurement/submitDecides a cart: a direct order, a purchase request, or a refusal. Needs cart_id and items.
Submit a stored cartPOST /v1/procurement/submit-cartChecks out a cart stored in the carts app, by id. Needs cart_id. Takes cost_center_id, the cost center every line books to, and customer_order_number.
  • Use submit when your checkout sends the cart itself. The App looks up the buyer's role in Customers for the contact_id in the request, and reads each line's cost center from the request. Without contact_id, role conditions see no role.
  • Each items element needs name (text) and quantity (a number). The other item fields are optional. A line names its cost center by code, in cost_center.
  • Submitting the same cart again is safe. cart_id identifies the submission. A repeat answers 200 with idempotent: true and what the first call made. A repeat for a request already declined or cancelled is refused with 409 decided; start a new cart. A repeat while the first call is still placing its direct order can be refused with 409; retry it. On a timeout, retry with the same cart_id.
  • submit-cart decides the stored cart exactly as submit does. A cart that is not active is refused with 409 and cart_not_active. A re-submit answers the order or request already made for it.
StatusoutcomeAlso returnsWhat it means
201directorderThe order is placed (unless order_pending: true, see below) and its budget is spent.
201approvalpurchase_request, pending_approvalsSent for approval. The budget is reserved while it waits.
409preventprevent.rule_id, prevent.reasonA rule blocks the purchase. Nothing is created.
409no_budgetno_budget.cost_center_code, no_budget.reasonA monetary cost center has no active budget. Nothing is created.
409currency_mismatchcurrency_mismatch.requested, .actualThe cart's currency differs from the cost center's. Nothing converts, so nothing is created.
409decidedpurchase_request, decided.statusThe cart's request was already declined or cancelled. Start a new cart.
  • budget_pending: true on a 201 means the order or request stands, but its budget step failed after it was made. budget_error says why. A direct order's commit is retried by the hourly sweep, by POST .../direct-orders/{id}/commit, or by a re-submit of the cart.
  • order_pending: true on a direct answer (201, or 200 on a repeat) means the Orders app accepted the order but holds it for its own approval, so it is not placed yet (order status pending). The budget is already spent. order_error is null while the Orders app holds the order for its own approval; otherwise it says that the order's status could not be read as placed. If the Orders app rejects the order, its budget is given back, as a cancellation does. The cart is marked ordered all the same. The fields are absent otherwise.
  • A 502 means another App refused a step. The message names that App.
  • 400 with an unknown cost center code means a line names a cost center that is not on this tenant, or is excluded for this punchout session.

Approval rules

OperationPathUse
ListGET /v1/procurement/approval-rulesThe rules.
CreatePOST /v1/procurement/approval-rulesAdd a rule.
ReadGET /v1/procurement/approval-rules/{id}One rule.
UpdatePUT /v1/procurement/approval-rules/{id}Edit a rule.
DeleteDELETE /v1/procurement/approval-rules/{id}Remove a rule.

A rule is condition, then effect, then approver:

  • condition (as of 2026-10): always, constantLimit, availableBudget, personalLimit, contactHasRole or contactMissingPermission. availableBudget and personalLimit ask Cost centers for the fact and fire when it is not satisfied. A rule with availableBudget must be scoped to a cost center.
  • effect (as of 2026-10): pendingOrder sends the cart to approval, prevent blocks it, and sendEmail only notifies and never gates. A prevent outranks every other effect.
  • approver_type: contact names one contact in approver_contact_id, role names a role in approver_role, and default resolves to the cost center's accountable_contact_id. On a global rule, or a center without one, anyone whose role grants orders.approve can decide it.
  • Global or per cost center. A rule with no cost_center_id measures the whole cart. A rule with one measures only the lines booked to that center, and does nothing when the cart books nothing there.
  • sequence orders the approvals within one cost center. The lowest is the first sign-off. Rules with no cost center form their own chain.

Purchase requests

OperationPathUse
ListGET /v1/procurement/purchase-requestsThe requests.
ReadGET /v1/procurement/purchase-requests/{id}One request.
UpdatePUT /v1/procurement/purchase-requests/{id}Edit a request.
Super-approvePOST /v1/procurement/purchase-requests/{id}/approveClears every outstanding approval at once and places the order; the request becomes ordered, stays approved with order_pending while the Orders app holds the order, or becomes cancelled if the Orders app calls the order off. An override for the Cockpit, the merchant's back office.
CancelPOST /v1/procurement/purchase-requests/{id}/cancelCancels a pending request and withdraws its budget. Takes an optional reason.
OrderPOST /v1/procurement/purchase-requests/{id}/orderRetry-safe approved to ordered: places the order, or reads back one already placed; confirms the budget once the order is placed. Safe to repeat.
  • Status (as of 2026-10): pending, approved, ordered, declined or cancelled. approved is a request every approval has cleared, with no placed order yet. ordered means the order exists.
  • Editing. The spend (totals, lines, cart and contact) can change only while the request is pending; after that the call is refused with 409. Reference numbers, notes, the buyer snapshot and the addresses can be corrected in any state, but the addresses not once the request is ordered.
  • Cancel is for pending requests. It is a back-office override, and a buyer can also cancel their own pending request in the storefront.
  • Super-approve is the operator's override, not an approval by a named approver. Called as a signed-in contact, it needs a role that grants both organization.manage and orders.approve (the built-in admin role does).
  • A request that is approved with no placed order has one of two causes. Placing the order failed after the decision: the decision stands, and the platform retries it hourly. To finish it at once, call .../order. Or the Orders app holds the order for its own approval: the decision call answers order_pending: true with order_error: null, the request keeps its order_id, and it waits. It becomes ordered on the next hourly sweep or .../order call after the Orders app places the order. If the Orders app calls the order off first, the request becomes cancelled, its budget reservation is released, and the purchase_request.cancelled event carries order_id, the order's cancellation_kind and its reason. Calling .../order while the order waits reads it back: it never places a second order and never approves it in the Orders app. It answers 200 with order_pending: true while the order still waits. When order_pending is true for another reason, order_error says why the order is not placed yet (the order could not be placed, or its status could not be read). The hourly sweep retries.

Purchase request items

OperationPathUse
ListGET /v1/procurement/purchase-request-itemsThe lines.
CreatePOST /v1/procurement/purchase-request-itemsAdd a line to a request.
ReadGET /v1/procurement/purchase-request-items/{id}One line.
UpdatePUT /v1/procurement/purchase-request-items/{id}Edit a line.
DeleteDELETE /v1/procurement/purchase-request-items/{id}Remove a line.

A line can be added, changed or removed only while its request is pending. Once decided, the call is refused with 409, because the lines are the spend that was decided.

Open request items

OperationPathUse
ListGET /v1/procurement/open-request-itemsThe lines of open purchase requests that name a product. Needs product_id; takes an optional status.

Purchase request events

OperationPathUse
ListGET /v1/procurement/purchase-request-eventsThe outcome events of each request.
ReadGET /v1/procurement/purchase-request-events/{id}One event.

Each event carries a name: purchase_request.requested, .approved, .declined, .cancelled, .ordered, .notification, budget.withdrawn or budget.confirmed. To react to them, subscribe to purchase_request_event.created (see the purchase-approvals guide).

Pending approvals

OperationPathUse
ListGET /v1/procurement/pending-approvalsThe approvals.
ReadGET /v1/procurement/pending-approvals/{id}One approval.
ApprovePOST /v1/procurement/pending-approvals/{id}/approveApproves one layer. Takes an optional reason.
DeclinePOST /v1/procurement/pending-approvals/{id}/declineDeclines one layer and ends the whole request. Takes an optional reason.
  • One approval is one approver's decision on one layer of a request, for one cost center. Approvals in the same cost center run in sequence order. Only the lowest pending one can be decided. Deciding a later one first is refused with 409.
  • Approving unlocks the next layer. The request becomes approved only when every layer, in every cost center, is approved.
  • Declining ends the whole request. It becomes declined and its reserved budget goes back. The decline reason is stored on the request as decline_reason.
  • Status (as of 2026-10): pending, approved, declined or cancelled.
  • Who decides. A decision is a person's act. The approver decides signed in as themselves, which is why the approval screen belongs in your storefront's account area (the block catalog has an account_approvals block). Only the contact a contact approval names may decide it, and only a holder of the role for a role approval. Both need a role that grants orders.approve (the built-in approver and admin roles do). A call not made as the approving contact is refused with 403.
  • The Cockpit holds the two overrides on the whole request: super-approve and cancel, described under Purchase requests.

Direct orders

OperationPathUse
ListGET /v1/procurement/direct-ordersThe direct orders.
ReadGET /v1/procurement/direct-orders/{id}One direct order.
CommitPOST /v1/procurement/direct-orders/{id}/commitRetries the budget commit. Refused with 409 unless the state is commit_pending or commit_refused.
SettlePOST /v1/procurement/direct-orders/{id}/settleSettles a refused commit by hand, with a required note. A back-office override. Refused with 409 unless the state is commit_refused. The budget is then never booked for that order.
  • Status (as of 2026-10): placing, commit_pending, commit_refused, committed, settled or abandoned.
  • Commit retries the movement that the submit call recorded.
  • The hourly sweep retries a commit_pending one; a commit_refused one waits for a commit or settle call.

Budget releases

OperationPathUse
ListGET /v1/procurement/budget-releasesThe releases.
ReadGET /v1/procurement/budget-releases/{id}One release.
RetryPOST /v1/procurement/budget-releases/{id}/retryRetries giving back a cancelled order's budget. Refused with 409 unless the state is pending or refused.
SettlePOST /v1/procurement/budget-releases/{id}/settleSettles a refused release by hand, with a required note. A back-office override. Refused with 409 unless the state is refused. The budget then stays booked for that order.
  • Status (as of 2026-10): pending, released, skipped, refused or settled.
  • A release gives back the budget of an order that was cancelled. The sweep retries releases that are still pending.

Reconcile

OperationPathUse
SweepPOST /v1/procurement/reconcileOne bounded reconciliation pass. Takes an optional limit, the number of rows to examine per status (default 50, at most 200).

The pass completes requests stuck between approved and ordered, releases orphaned budget reservations, records any outcome whose write failed, finishes direct orders whose commit is owed, and retries budget releases. The answer also lists two arrays of purchase request ids: awaiting_order (approved requests whose order the Orders app still holds for its own approval; they wait and are not failures; a request whose order status could not be read is also listed under failed) and cancelled (approved requests cancelled because the Orders app called their order off before it was placed; their reservation is released). The platform runs it hourly (as of 2026-10), so you rarely need to call it yourself.

Vocabularies

OperationPathUse
IndexGET /v1/procurement/vocabulariesThe names of the value sets, with their titles, without their values.
One vocabularyGET /v1/procurement/vocabularies/{name}Every value of one set, in the order the App enforces, with its title and badge tone. An unknown name is answered 404.

Read the sets instead of keeping a copy of the lists.

Was this page helpful?