Price lists
Everything else in the prices app configures prices. POST /v1/prices/resolve is the one route that answers them, and a storefront reaches it on every listing, every product page and every cart.
Base path: https://api.revenexx.com/v1/prices.
The list
curl "https://api.revenexx.com/v1/prices/lists?limit=50" \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..."
| Field | Meaning |
|---|---|
code | Unique per tenant. What an import, an ERP export and every integration addresses it by, and what the default_price_list_code setting names. |
name, labels, description | Operator-facing. description is never shown to a buyer. |
currency | ISO 4217, and the currency of every amount in the list — entries carry none of their own. |
tax_basis | net or gross. The one fact a price cannot be without. Null inherits the market's answer. |
status | Only active lists are candidates. inactive retires a list without deleting its prices. |
valid_from, valid_until | The whole list's validity window. Outside it, the list is not a candidate at all. |
contact_id | Buyer scope: this one contact. The most specific scope there is. |
organization_id | Buyer scope: buyers of this organization. |
channel_id | Buyer scope: this sales channel. |
requires_auth | When true, the list resolves only for a buyer with a contact or organization context. An anonymous resolve never matches it. |
priority | Tie-break within one specificity group, higher first. |
is_default | The fallback list. Within its group it deliberately sorts last. |
metadata | Free-form, never read by this app. |
tax_included is a legacy mirror of tax_basis. Only true is read as a statement (gross); false is the column default and is not read as anybody having chosen net.
POST /v1/prices/lists/defaults seeds the standard list. POST /v1/prices/lists/{id}/make-default moves the default flag in one operation.
The entries
An entry is one rung of one item's quantity ladder.
curl -X POST "https://api.revenexx.com/v1/prices/lists/{list_id}/entries" \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..." \
-H "Content-Type: application/json" \
-d '{"sku":"ACME-4711-BLK","quantity_min":1,"unit_price":19.90,"unit":"pcs"}'
| Field | Meaning |
|---|---|
product_id / sku | An entry needs one of the two. sku is matched exactly — never normalised or case-folded. |
quantity_min | Lowest quantity this price applies from. |
unit_price | Price for one unit of unit, in the list's currency and on the list's tax basis. A decimal in major units (19.90), never minor units. |
unit | Free text — pcs, m, kg, a packaging size. This app neither validates nor converts it. |
price_type | standard is a number. on_request is the explicit no-price marker. |
valid_from, valid_until | The entry's own window. This is how a promo price is expressed: a second rung for the same item and quantity, live only inside its window. |
The ladder for one item is the set of entries sharing its identity, and the rung with the highest quantity_min at or below the requested quantity wins. Below the first rung, the first rung applies.
Writing entries in bulk
| Route | Does |
|---|---|
PUT /v1/prices/lists/{list_id}/entries | Replace all entries of the list. |
POST /v1/prices/lists/{list_id}/entries/bulk | Bulk-upsert entries. |
POST /v1/prices/lists/{list_id}/entries/adjust | Bulk-change prices — a percentage or absolute move. Only writes the rows whose price actually moved, so updated_at stays a real "the price changed here" marker. |
POST /v1/prices/lists/{list_id}/entries/ladder | Generate a quantity ladder for one item, instead of posting each rung. |
Resolving a price
curl -X POST https://api.revenexx.com/v1/prices/resolve \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..." \
-H "X-Revenexx-Market: de" \
-H "Content-Type: application/json" \
-d '{
"currency": "EUR",
"contact_id": "…",
"organization_id": "…",
"channel_id": "…",
"items": [
{ "sku": "ACME-4711-BLK", "quantity": 12 },
{ "sku": "ACME-9001", "quantity": 1 }
]
}'
Up to 200 items per call — a whole cart or a whole listing in one round trip. The answer holds one entry per item, in request order, with the unit price this buyer pays, the net/gross pair, the tax rate, the list that decided it, and that item's full quantity ladder.
at sets the instant every validity window is evaluated at, which is how a promo price is previewed before it starts.
The resolution order, in full
This is the value of the app, and it is not guessable from the field types.
1. Candidates. A list is a candidate when it is active, its currency equals the currency of the call, the instant at falls inside its validity window, it is visible in the buyer's market, and its buyer scope matches or is open. A requires_auth list is dropped for a buyer with neither contact_id nor organization_id.
currency is not a rounding problem — it is an empty answer.The X-Revenexx-Market header scopes which lists are visible; lists assigned to no market are global and always visible.
2. Specificity decides first, and priority never overrules it.
| Scope | Level |
|---|---|
| contact-scoped | 4 |
| organization-scoped | 3 |
| channel-scoped | 2 |
| open | 0 |
An organization list at priority: 0 therefore wins over an open list at priority: 100.
3. Within one specificity level: priority descending, then non-default before default — the default list is deliberately last, so it prices only what nothing else did.
4. A genuine tie (same specificity, same priority, same default flag) is settled by the tenant's price_list_priority_tiebreak setting — lowest_price, highest_price, newest or code — never by the order the database happened to return rows in. The setting in force is echoed in basis.price_list_priority_tiebreak.
5. The first list that prices the item wins, and the search stops there — even if a later, less specific list is cheaper. Its full tier ladder comes back in tiers.
6. An on_request entry stops the search too, and inside a tie it outranks every price: a list that says "ask us" for this buyer is authoritative and cannot be undercut by a list that happens to sort after it.
7. Nothing found → on_request, never 0, with a reason:
not_priced on_request_entry anonymous_denied no_identity
A storefront shows "price on request". It must never show €0.
Amounts
unit_price is per one unit of the entry's unit, in currency, as a decimal in major units (19.90) — never cents — and on the basis tax_basis names. tax_basis comes from the list's own column, else a legacy tax_included: true on it, else the tenant's answer.
Where to go next
- Markets — the currency and tax class a price resolves against.
- Channels — channel scope, specificity level 2.
- Organizations — the
price_listcode on a company. - Carts —
price_snapshot_modeat conversion.