Approvals
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
| Operation | Path | Use |
|---|---|---|
| Submit | POST /v1/procurement/submit | Decides a cart: a direct order, a purchase request, or a refusal. Needs cart_id and items. |
| Submit a stored cart | POST /v1/procurement/submit-cart | Checks 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
submitwhen your checkout sends the cart itself. The App looks up the buyer's role in Customers for thecontact_idin the request, and reads each line's cost center from the request. Withoutcontact_id, role conditions see no role. - Each
itemselement needsname(text) andquantity(a number). The other item fields are optional. A line names its cost center by code, incost_center. - Submitting the same cart again is safe.
cart_ididentifies the submission. A repeat answers200withidempotent: trueand what the first call made. A repeat for a request already declined or cancelled is refused with409decided; start a new cart. A repeat while the first call is still placing its direct order can be refused with409; retry it. On a timeout, retry with the samecart_id. submit-cartdecides the stored cart exactly assubmitdoes. A cart that is not active is refused with409andcart_not_active. A re-submit answers the order or request already made for it.
| Status | outcome | Also returns | What it means |
|---|---|---|---|
201 | direct | order | The order is placed (unless order_pending: true, see below) and its budget is spent. |
201 | approval | purchase_request, pending_approvals | Sent for approval. The budget is reserved while it waits. |
409 | prevent | prevent.rule_id, prevent.reason | A rule blocks the purchase. Nothing is created. |
409 | no_budget | no_budget.cost_center_code, no_budget.reason | A monetary cost center has no active budget. Nothing is created. |
409 | currency_mismatch | currency_mismatch.requested, .actual | The cart's currency differs from the cost center's. Nothing converts, so nothing is created. |
409 | decided | purchase_request, decided.status | The cart's request was already declined or cancelled. Start a new cart. |
budget_pending: trueon a201means the order or request stands, but its budget step failed after it was made.budget_errorsays why. A direct order's commit is retried by the hourly sweep, byPOST .../direct-orders/{id}/commit, or by a re-submit of the cart.order_pending: trueon adirectanswer (201, or200on a repeat) means the Orders app accepted the order but holds it for its own approval, so it is not placed yet (order statuspending). The budget is already spent.order_errorisnullwhile 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
502means another App refused a step. The message names that App. 400with 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
| Operation | Path | Use |
|---|---|---|
| List | GET /v1/procurement/approval-rules | The rules. |
| Create | POST /v1/procurement/approval-rules | Add a rule. |
| Read | GET /v1/procurement/approval-rules/{id} | One rule. |
| Update | PUT /v1/procurement/approval-rules/{id} | Edit a rule. |
| Delete | DELETE /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,contactHasRoleorcontactMissingPermission.availableBudgetandpersonalLimitask Cost centers for the fact and fire when it is not satisfied. A rule withavailableBudgetmust be scoped to a cost center.effect(as of 2026-10):pendingOrdersends the cart to approval,preventblocks it, andsendEmailonly notifies and never gates. Apreventoutranks every other effect.approver_type:contactnames one contact inapprover_contact_id,rolenames a role inapprover_role, anddefaultresolves to the cost center'saccountable_contact_id. On a global rule, or a center without one, anyone whose role grantsorders.approvecan decide it.- Global or per cost center. A rule with no
cost_center_idmeasures the whole cart. A rule with one measures only the lines booked to that center, and does nothing when the cart books nothing there. sequenceorders the approvals within one cost center. The lowest is the first sign-off. Rules with no cost center form their own chain.
Purchase requests
| Operation | Path | Use |
|---|---|---|
| List | GET /v1/procurement/purchase-requests | The requests. |
| Read | GET /v1/procurement/purchase-requests/{id} | One request. |
| Update | PUT /v1/procurement/purchase-requests/{id} | Edit a request. |
| Super-approve | POST /v1/procurement/purchase-requests/{id}/approve | Clears 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. |
| Cancel | POST /v1/procurement/purchase-requests/{id}/cancel | Cancels a pending request and withdraws its budget. Takes an optional reason. |
| Order | POST /v1/procurement/purchase-requests/{id}/order | Retry-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,declinedorcancelled.approvedis a request every approval has cleared, with no placed order yet.orderedmeans 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 with409. Reference numbers, notes, the buyer snapshot and the addresses can be corrected in any state, but the addresses not once the request isordered. - Cancel is for
pendingrequests. 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.manageandorders.approve(the built-inadminrole does). - A request that is
approvedwith 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 answersorder_pending: truewithorder_error: null, the request keeps itsorder_id, and it waits. It becomesorderedon the next hourly sweep or.../ordercall after the Orders app places the order. If the Orders app calls the order off first, the request becomescancelled, its budget reservation is released, and thepurchase_request.cancelledevent carriesorder_id, the order'scancellation_kindand itsreason. Calling.../orderwhile the order waits reads it back: it never places a second order and never approves it in the Orders app. It answers200withorder_pending: truewhile the order still waits. Whenorder_pendingis true for another reason,order_errorsays 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
| Operation | Path | Use |
|---|---|---|
| List | GET /v1/procurement/purchase-request-items | The lines. |
| Create | POST /v1/procurement/purchase-request-items | Add a line to a request. |
| Read | GET /v1/procurement/purchase-request-items/{id} | One line. |
| Update | PUT /v1/procurement/purchase-request-items/{id} | Edit a line. |
| Delete | DELETE /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
| Operation | Path | Use |
|---|---|---|
| List | GET /v1/procurement/open-request-items | The lines of open purchase requests that name a product. Needs product_id; takes an optional status. |
Purchase request events
| Operation | Path | Use |
|---|---|---|
| List | GET /v1/procurement/purchase-request-events | The outcome events of each request. |
| Read | GET /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
| Operation | Path | Use |
|---|---|---|
| List | GET /v1/procurement/pending-approvals | The approvals. |
| Read | GET /v1/procurement/pending-approvals/{id} | One approval. |
| Approve | POST /v1/procurement/pending-approvals/{id}/approve | Approves one layer. Takes an optional reason. |
| Decline | POST /v1/procurement/pending-approvals/{id}/decline | Declines 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
sequenceorder. Only the lowest pending one can be decided. Deciding a later one first is refused with409. - Approving unlocks the next layer. The request becomes
approvedonly when every layer, in every cost center, is approved. - Declining ends the whole request. It becomes
declinedand its reserved budget goes back. The decline reason is stored on the request asdecline_reason. - Status (as of 2026-10):
pending,approved,declinedorcancelled. - 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_approvalsblock). Only the contact acontactapproval names may decide it, and only a holder of the role for aroleapproval. Both need a role that grantsorders.approve(the built-inapproverandadminroles do). A call not made as the approving contact is refused with403. - The Cockpit holds the two overrides on the whole request: super-approve and cancel, described under Purchase requests.
Direct orders
| Operation | Path | Use |
|---|---|---|
| List | GET /v1/procurement/direct-orders | The direct orders. |
| Read | GET /v1/procurement/direct-orders/{id} | One direct order. |
| Commit | POST /v1/procurement/direct-orders/{id}/commit | Retries the budget commit. Refused with 409 unless the state is commit_pending or commit_refused. |
| Settle | POST /v1/procurement/direct-orders/{id}/settle | Settles 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,settledorabandoned. - Commit retries the movement that the submit call recorded.
- The hourly sweep retries a
commit_pendingone; acommit_refusedone waits for a commit or settle call.
Budget releases
| Operation | Path | Use |
|---|---|---|
| List | GET /v1/procurement/budget-releases | The releases. |
| Read | GET /v1/procurement/budget-releases/{id} | One release. |
| Retry | POST /v1/procurement/budget-releases/{id}/retry | Retries giving back a cancelled order's budget. Refused with 409 unless the state is pending or refused. |
| Settle | POST /v1/procurement/budget-releases/{id}/settle | Settles 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,refusedorsettled. - A release gives back the budget of an order that was cancelled. The sweep retries releases that are still pending.
Reconcile
| Operation | Path | Use |
|---|---|---|
| Sweep | POST /v1/procurement/reconcile | One 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
| Operation | Path | Use |
|---|---|---|
| Index | GET /v1/procurement/vocabularies | The names of the value sets, with their titles, without their values. |
| One vocabulary | GET /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.