Vendor Integration Standard
The canonical contract for integrating with the Linra Omni vendor API — auth, capabilities, every door, idempotency, rate limits, status vocabularies, and onboarding.
This document is the single canonical reference for integrating with the Linra Omni vendor API. If something in a guide or the API Reference ever seems to disagree with this page, this page wins — file it as a docs bug.
1. Who you are, and who you are not
Section titled “1. Who you are, and who you are not”A vendor is a supplier Linra sources physical product from (e.g. a perfume distributor). This
is a different relationship, a different identity plane, and a different API from a partner (a
business reselling through Linra Omni). If you were told you’re integrating as a partner, you are
in the wrong docs — go to the
Linra Omni Developer Docs instead. The two credential types are
issued from the same identity provider but are cryptographically distinct: a vendor token carries
audience vendor-api and is rejected by every partner-facing route; a partner token carries
audience partner-api and is rejected by every vendor-facing route.
2. Authentication
Section titled “2. Authentication”POST /api/v1/auth/token exchanges a clientId + clientSecret for a short-lived JWT
bearer token (audience vendor-api). Send it as Authorization: Bearer <token> on every other
request on the vendor Gateway (global routes under /api/v1, commercial routes under
/api/v1/scent).
- Lifetime: 5 minutes, no refresh-token grant — re-authenticate with your long-lived
clientId/clientSecretinstead of holding a token indefinitely. See Authentication & capabilities for the caching pattern. - No delegation. Unlike the partner plane’s
onBehalfOf, a vendor credential has no hierarchy to act on behalf of — a vendor is a single, flat identity. - Capabilities are baked into the token. Your credential is issued with a specific set of capabilities (see §3); the issued token’s claims carry exactly that set, and every vendor endpoint checks it from the token alone — no additional lookup.
- Failure modes: a wrong
clientId/clientSecret, or an inactive vendor, is401(the SAME genericUNAUTHORIZED_INVALID_CREDENTIALS/UNAUTHORIZED_VENDOR_INACTIVEshape either way — never distinguishable, so a caller can’t enumerate valid client ids); more than 10 requests in 60 seconds is429with aRetry-Afterheader (§6).
3. Capabilities — the five independently-grantable doors
Section titled “3. Capabilities — the five independently-grantable doors”Every vendor credential is issued with a subset of five capabilities. They are independently grantable — a credential can hold any combination, including exactly one.
| Capability | Grants access to | Endpoints |
|---|---|---|
| Offers | Submit and edit your own product offers | GET/POST /api/v1/scent/offers, GET/PATCH /api/v1/scent/offers/{id} |
| Cost | Push a cost change for one of your offers | PATCH /api/v1/scent/offers/{id}/cost |
| Stock | Push declared availability/quantity for one of your offers | PATCH /api/v1/scent/offers/{offerId}/stock |
| Orders | Poll your assigned fulfilment groups, push fulfilment status | GET /api/v1/scent/orders, GET/PATCH /api/v1/scent/orders/{groupId}[/status] |
| Account | Manage your own API credentials (writes only) | POST /api/v1/credentials, PATCH /api/v1/credentials/{id}/active, DELETE /api/v1/credentials/{id} |
A credential holding only Stock, for example, can push availability updates but cannot read or
edit offers, push cost, or see any order data — each door checks its OWN required capability, and a
missing one is rejected with 403 FORBIDDEN_CAPABILITY_NOT_GRANTED, never silently ignored or
downgraded to a partial response. Account gates ONLY the credential-write actions — reading your
own profile (GET /api/v1/profile) and listing your own credentials (GET /api/v1/credentials)
require no capability at all, since reading your own compliance/credential status carries none of
the risk writing it would.
Webhook subscription management (/api/v1/scent/webhooks/**, §7) does NOT ride a single fixed
capability at all — even on create/update. Each event type you subscribe to is checked against its
own natural owner: vendor.scent.order.assigned requires Orders; vendor.scent.offer.review-decision
requires Offers. An Offers-only credential can subscribe to its own offer-review-decision
notifications without ever holding Orders. Every OTHER webhook action (list/get/update the URL
only/deactivate/rotate-secret/delete/test-fire/list deliveries) requires no capability at all —
ownership of the subscription is the only gate.
4. The five-door model
Section titled “4. The five-door model”If you’ve read integration docs for other B2B APIs, you may expect one big “vendor account” resource. Linra Omni instead exposes five narrow, single-purpose doors, each doing exactly one thing:
- Offers (
/api/v1/scent/offers) — the identity/cost/stock facts about a product you supply, submitted through a review workflow. Nothing here is ever live until an internal reviewer approves it. - Cost (
/api/v1/scent/offers/{id}/cost) — a narrow sibling action on the SAME offer resource, but gated by its own capability so a Cost-only integration never needs Offers access. Always routes through the same approval pipeline as an Offers-capability cost edit. - Stock (
/api/v1/scent/offers/{offerId}/stock) — applied INSTANTLY, no review gate. Availability is a real-world fact, not a commercial term, so it doesn’t wait on a reviewer. A sub-resource of the offer it declares availability for, not a sibling top-level resource. - Orders (
/api/v1/scent/orders,/api/v1/scent/webhooks) — read-only visibility into what you’ve been assigned to fulfil, plus a narrow status-push action and webhook subscription management. You can never see an order, a partner, or another vendor’s data through this door — only your own assigned fulfilment groups. - Account (
/api/v1/credentials) — self-service credential lifecycle for your OWN vendor: generate a new credential (capped at the union of what you already hold — see §11), deactivate, or revoke one.Profile(/api/v1/profile) is the read-only sibling of this door — always reachable with no capability, since there is nothing to write there on this API at any capability level (§9).
Every door delegates to the exact same internal write path Linra’s own catalog/fulfilment teams use — you are not on a second-class “integration API,” you are calling the same machinery through a narrower, capability-gated front door.
5. Idempotency & retries
Section titled “5. Idempotency & retries”- Offer creation is idempotent when you set
vendorExternalId. Re-submitting the SAMEvendorExternalIdreturns the already-created offer unchanged — safe to retry after a timeout or an unknown outcome. Omit it and everyPOSTcreates a new offer. - Cost and stock pushes are naturally idempotent by overwrite. Re-pushing the identical value is a pure no-op (stock) or overwrites the same pending value (cost) — never a duplicate.
- Fulfilment status pushes are idempotent and never error on a stale/backward/duplicate/invalid
target. A push that can’t apply returns
200withapplied: falseand the group’s unchanged current state — build your retry logic to treat this as informational, not a failure. - Build a single retry-with-backoff policy at your HTTP client layer for
5xxand429responses, honoringRetry-Afteron a 429 (see §6). Never retry a4xxother than429without changing the request — those are telling you the request itself is wrong.
6. Rate limits
Section titled “6. Rate limits”Every route on the vendor Gateway is rate-limited using a sliding window (evaluated over 4 sub-segments, so a burst at a window boundary is smoothed, not doubled), partitioned per authenticated caller.
| Route | Limit | Window |
|---|---|---|
POST /api/v1/auth/token |
10 requests | 60 seconds |
| Every other route | 120 requests | 60 seconds |
A 429 never includes remaining-quota detail beyond the Retry-After header (seconds) — respect
it rather than guessing a retry interval. If you’re polling GET /api/v1/scent/orders for new
assignments, use the changedSince delta-sync parameter (§7) rather than re-fetching the full list
on every poll, and prefer the webhook (§8) over polling at all if your Orders integration is
latency-sensitive.
7. Status vocabularies
Section titled “7. Status vocabularies”Offer status
Section titled “Offer status”Draft → PendingMatch/PendingReview → Approved → (Suspended | Withdrawn), or Rejected from any
pending state. You never set Approved yourself — that’s always an internal reviewer action. A
Draft offer is a save-for-later with no review-queue entry yet.
| Status | Meaning |
|---|---|
Draft |
Saved, not yet submitted for review. |
PendingMatch |
Submitted without a variantId — awaiting a reviewer to match or create the canonical catalog entry. |
PendingReview |
Submitted with a proposed match — awaiting reviewer approval. |
Approved |
Live. A cost push here becomes a pending change (see below), never overwrites the live cost directly. |
Rejected |
The reviewer rejected the submission, with a reason. |
Withdrawn |
Withdrawn (by you or an internal action). |
Suspended |
Temporarily paused by an internal reviewer. |
An Approved offer’s cost.pending object is non-null exactly when a cost push is awaiting
reviewer approval — poll it, or subscribe to vendor.scent.offer.review-decision (§8), to learn
the outcome.
Fulfilment status
Section titled “Fulfilment status”Pending → Packed → Shipped → Delivered, or Cancelled from Pending/Shipped, or Returned
after delivery. You may only ever push Shipped, Delivered, or Cancelled — Packed is an
internal operator-only transition (Linra confirms packing before you ship), and Returned is
customer-return-driven, never vendor-initiated. Pushing any other target, or a backward/stale/
already-applied one, is a clean no-op (applied: false), never an error — see
Orders & fulfilment status for the full transition table and
worked examples.
8. Webhooks
Section titled “8. Webhooks”Instead of polling GET /api/v1/scent/orders, subscribe to push notifications for two event
types (both under /api/v1/scent/webhooks):
| Event type | Fired when | Required capability to subscribe |
|---|---|---|
vendor.scent.order.assigned |
A fulfilment group is newly assigned to you (at order creation, or a later manual/auto sourcing decision). | Orders |
vendor.scent.offer.review-decision |
One of your offers is approved, rejected, or its pending cost change is approved/rejected. | Offers |
Both event types carry the scent. product segment (matching the platform’s product-scoped naming
convention) and are checked independently: an Offers-only credential can subscribe to
vendor.scent.offer.review-decision without ever holding Orders. Payloads are deliberately
minimal — an assignment notification carries only the group id; fetch full shipping/line details
via GET /api/v1/scent/orders/{groupId}. See
Webhooks & HMAC verification for the full envelope shape, the signature
scheme, a verified code sample, and retry/backoff behavior.
9. What’s explicitly out of scope today
Section titled “9. What’s explicitly out of scope today”- No vendor-visible partner/order-commercial data, ever. Order totals, commission, VAT breakdown, discount codes, the reselling partner’s identity, and Linra’s own cost-floor/margin policy are permanently out of scope for this API by design (§4) — this is not a “not yet built” gap, it never will be.
- No profile write path, on any plane, at any capability level.
GET /api/v1/profileis read-only, permanently — there is noPUT/PATCHon this resource. Vendor profile edits (including tax/VAT registration, already internal-admin-only before this) are exclusively an internal-admin action, performed via the Linra Omni Portal dashboard. This is distinct from credential management (§3/§11), which IS self-service on this API via theAccountcapability.
10. Service levels
Section titled “10. Service levels”The specific numeric commitments below are TBD — to be set by Linra business/ops. The structure (what will be measured, and how it will be communicated) is fixed; the numbers are not invented here.
| Metric | Target |
|---|---|
| API availability (vendor Gateway, every route) | TBD — to be set by Linra business/ops |
| Offer review turnaround (submission → Approved/Rejected) | TBD — to be set by Linra business/ops |
| Webhook delivery latency (event occurs → first delivery attempt) | Today: VendorWebhookDispatcher polls every 5 seconds — a soft, implementation-level figure, not a contractual SLA. A contractual latency target is TBD. |
| Support response time for integration issues | TBD — to be set by Linra business/ops |
11. Onboarding
Section titled “11. Onboarding”- Contact Linra to begin vendor onboarding (commercial + catalog-side setup — outside this
API’s scope). Linra creates your
Vendorrecord, a dashboard login for the Linra Omni Portal (a separate, human-operated login — not part of this API), and your first API credential — with the capability set (Offers/Stock/Cost/Orders/Account, any combination) your onboarding conversation established. The first credential for a new vendor is always Linra-issued; self-service generation requires at least one already-active credential to establish a baseline and is rejected otherwise (422 BUSINESS_VENDOR_FIRST_CREDENTIAL_REQUIRES_ONBOARDING). - Manage further credentials yourself. Once you hold at least one active credential holding
Account, generate additional ones self-service — directly on this API viaPOST /api/v1/credentials, or under Administration → API Credentials on the Portal dashboard at https://omni-portal.linra.net (both surfaces enforce the identical rule) — e.g. a second credential for a different environment, or to rotate a compromised one (generate new, then deactivate the old viaPATCH /api/v1/credentials/{id}/active— the same zero-downtime pattern the partner API’s own credential guidance describes;DELETE /api/v1/credentials/{id}permanently revokes one). Self-service generation can never grant a capability beyond the union of what your OTHER active credentials already hold — requesting a superset is rejected (403 FORBIDDEN_CAPABILITY_NOT_GRANTED_TO_VENDOR). Need a genuinely new capability (e.g. you were onboarded Offers-only and now also need Orders)? That still goes through your onboarding contact, not self-service. - Exchange the credential for a token (§2) and confirm you can call an endpoint matching a granted capability, and that a NON-granted capability correctly 403s — see Getting started.
- Submit a test offer (Offers capability) and confirm it appears in Linra’s internal review queue.
- If you hold the Orders capability, subscribe to
vendor.scent.order.assigned(§8) or set up polling withchangedSince(§7), and usePOST /api/v1/scent/webhooks/{id}/test-fireto prove your endpoint receives and correctly verifies a signed delivery before going live. - Move to production once staging is proven end-to-end — request a production credential with the same capability set.
12. Change control
Section titled “12. Change control”This is a versioned facade (/api/v1, with commercial routes further scoped under
/api/v1/scent) — a breaking change to an existing endpoint’s request/response shape gets a new
version prefix, never an in-place breaking change to v1. See
the Changelog for what’s shipped since this document was first published.

