Authenticate with an API key
See the authentication docs for how to create an API key. Make sure the role you assign it can write transactions.
Create an order
POST /v1/transactions records one completed transaction, including its line items, payments, and totals, against a store. A minimal order for a single product paid by card looks like this:
- Line items are keyed by
store_product_id, the ID of the product in your Vori catalog, not by barcode.store_product_idis the ID that matters here: if you only have a barcode, resolve it to astore_product_idagainst the store products endpoint before submitting, since a barcode by itself isn’t accepted as a line item key. - Each line’s
totalmust equal(retail_price x quantity)pluspromo_savings,discount_total, anyitem_modifiersamounts, and that line’stax_total. Savings are signed: sendpromo_savingsanddiscount_totalas negative amounts, so adding them is what reduces the line. - For a product your catalog sells by weight, the line prices off
(retail_price x weight)instead: sendquantityas 1, send the measuredweight, and setretail_priceto the price per unit of weight. - At the transaction level,
tax_totalmust equal the sum of the line-item tax totals,totalmust equal the sum of the line totals, and the payments must add up tototal. - Every one of these is checked rather than accepted as reported, and a mismatch is rejected rather than silently corrected.
- Payments are recorded as given:
payment_type,amount, and your processor’s own reference inexternal_transaction_idare stored so your systems can look the payment up later, but none of it is interpreted or re-verified against a processor.
Idempotency
id is a UUIDv7 you generate, and it is the idempotency key for the request. Generate it once per order, before your first attempt, and reuse that same value on every retry. Sending the same id again returns the transaction already recorded rather than creating a duplicate. If you mint a new id every time you retry, a retry after a dropped response creates a second transaction instead of deduplicating against the first. The API can only recognize a replay if the replay carries the same key the original attempt did.
id is unique globally, but external_id (your own order number, like an e-commerce order ID) is scoped to your banner and is not required to be unique. Use it for lookups and reporting; use id for retry safety.
Conflicts
Sending the sameid twice can resolve two ways:
-
Identical request. Every compared field matches what was recorded the first time: same store, lane, employee, shopper, totals, line items, and payments. The endpoint returns the transaction already on file with a normal success response, not an error. This is the safe case to build retries around: on a timeout or a dropped connection, resend the exact same payload under the same
idand treat a success response as confirmation, whether or not the first attempt actually landed. -
Same
id, different values. The request names a transaction already recorded but disagrees with it on some field, such as a different total, a different line item, or a different payment. This returns a409 Conflicterror that names which fields don’t match, meaning the transaction is already recorded and what you just sent doesn’t describe it. This isn’t a transient failure, so retrying the same divergent payload will just keep returning a 409. If you see one outside of a deliberate retry, it meansidwas reused across two different orders in your own system, which is a bug on the calling side to fix rather than a condition to retry through.
Virtual lanes and employees
store_id is the only attribution field this endpoint requires. Vori automatically creates and maintains one virtual lane (per store) and one virtual employee (per banner) the first time an order is submitted through this API, and attributes the transaction to them when you don’t specify lane_id or employee_id yourself.
These virtual records aren’t provisioned like a physical lane or a real staff member. There’s no terminal or login behind them, and they are not editable. If you do pass lane_id or employee_id explicitly, it has to be the store’s virtual lane or virtual employee; naming a staffed lane or a real employee is rejected, since this API isn’t a substitute for completing a sale on an actual terminal.
Filter or group by lane_id in your own reporting (or in Vori’s) to separate transactions your system submitted from everything completed in-store. Every API-originated order carries the same virtual lane and employee, so this split is reliable.
Assumptions and caveats
- No promotion or rewards math yet. Vori doesn’t compute promotions, discounts, or loyalty rewards for transactions submitted this way.
promo_savings,discount_total, and any per-line fees are recorded exactly as you send them, with no independent calculation or validation against Vori’s own offer or loyalty configuration. Support for applying Vori promotions and loyalty rewards to API-submitted orders is planned; until then, don’t use promotions or loyalty rewards with orders submitted through this API. metadatais create-only. Whatever key/value pairs you attach at creation are stored and returned unchanged, but the transaction is otherwise immutable once recorded. There’s no update endpoint to revisemetadata, or anything else, after the fact. Constraints:- Up to 50 keys.
- Each key is 1-40 characters: letters, numbers, underscores, and hyphens.
- Values must be strings, up to 500 characters.
- Keys starting with
voriare reserved for Vori’s own use and are rejected if you try to write them.
- Totals are enforced, not inferred. As above, get the line-item and transaction-level math right on your side before submitting; the endpoint checks consistency rather than deriving one figure from the others.
Refund a transaction
A transaction can only be refunded if it’s a completed sale that was itself created through this API. Attempts to refund in-store transactions or non-sale transactions will be rejected.
POST /v1/transactions/{id}/refunds records a refund your own system processed against a transaction Vori already recorded, and returns it as a transaction of its own.
- The request mirrors what a refund reads back: quantities, weights, and the money coming back —
taxable_amount,tax_total, each linetotal, the transaction totals, and every paymentamount— are negative. - Three fields stay positive:
retail_price, because it is the price the item sold at rather than an amount moving; andpromo_savingsanddiscount_total, because they reverse the negative savings recorded on the original sale. - Each line and payment names the one it reverses (
transaction_line_item_id,transaction_payment_id). Retrieve the original transaction to read those IDs. - A line cannot return more than earlier refunds left it, checked component by component (quantity or weight, total, taxable amount, tax, promo savings, discount, and each fee or deposit), not just against the line’s overall total. A payment cannot return more than it has left either.
retail_priceon a refund line must match the price the product actually sold at; a refund cannot restate it.- Fees and deposits are refunded as they were charged on the original line, by
item_modifier_id, even if the fee has since been renamed or removed from your catalog. idis a UUIDv7 and works the same idempotency way as on the create-transaction endpoint: generate it once per refund, before your first attempt, and reuse it on every retry. Sending the same refund again under that ID returns the refund already recorded; sending different values under it is rejected.