Skip to content
Linra Omni Vendor Docs

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.

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.

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/clientSecret instead 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, is 401 (the SAME generic UNAUTHORIZED_INVALID_CREDENTIALS/UNAUTHORIZED_VENDOR_INACTIVE shape either way — never distinguishable, so a caller can’t enumerate valid client ids); more than 10 requests in 60 seconds is 429 with a Retry-After header (§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.

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.

  • Offer creation is idempotent when you set vendorExternalId. Re-submitting the SAME vendorExternalId returns the already-created offer unchanged — safe to retry after a timeout or an unknown outcome. Omit it and every POST creates 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 200 with applied: false and 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 5xx and 429 responses, honoring Retry-After on a 429 (see §6). Never retry a 4xx other than 429 without changing the request — those are telling you the request itself is wrong.

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.

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.

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.

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.

  • 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/profile is read-only, permanently — there is no PUT/PATCH on 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 the Account capability.

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
  1. Contact Linra to begin vendor onboarding (commercial + catalog-side setup — outside this API’s scope). Linra creates your Vendor record, 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).
  2. Manage further credentials yourself. Once you hold at least one active credential holding Account, generate additional ones self-service — directly on this API via POST /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 via PATCH /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.
  3. 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.
  4. Submit a test offer (Offers capability) and confirm it appears in Linra’s internal review queue.
  5. If you hold the Orders capability, subscribe to vendor.scent.order.assigned (§8) or set up polling with changedSince (§7), and use POST /api/v1/scent/webhooks/{id}/test-fire to prove your endpoint receives and correctly verifies a signed delivery before going live.
  6. Move to production once staging is proven end-to-end — request a production credential with the same capability set.

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.