Skip to content

Vendor Integration Standard

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/vendor/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 /api/v1/vendor request.

  • 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.

3. Capabilities — the four independently-grantable doors

Section titled “3. Capabilities — the four independently-grantable doors”

Every vendor credential is issued with a subset of four 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/vendor/offers, GET/PATCH /api/v1/vendor/offers/{id}
Cost Push a cost change for one of your offers PATCH /api/v1/vendor/offers/{id}/cost
Stock Push declared availability/quantity for one of your offers PATCH /api/v1/vendor/stock/{offerId}
Orders Poll your assigned fulfilment groups, push fulfilment status, and manage webhook subscriptions GET /api/v1/vendor/orders, GET/PATCH /api/v1/vendor/orders/{groupId}[/status], all of /api/v1/vendor/webhooks/**

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.

Webhook subscription management (§7) rides the Orders capability rather than a fifth one — a credential with no order/offer-decision data to subscribe to has no webhook to configure in the first place. If your integration is Offers-only and wants offer-review-decision push notifications without polling, ask your onboarding contact — this is a known, tracked gap (see §9).

If you’ve read integration docs for other B2B APIs, you may expect one big “vendor account” resource. Linra Omni instead exposes four narrow, single-purpose doors, each doing exactly one thing:

  • Offers (/api/v1/vendor/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/vendor/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/vendor/stock/{offerId}) — applied INSTANTLY, no review gate. Availability is a real-world fact, not a commercial term, so it doesn’t wait on a reviewer.
  • Orders (/api/v1/vendor/orders, /api/v1/vendor/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.

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 /api/v1/vendor route 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/vendor/auth/token 10 requests 60 seconds
Every other /api/v1/vendor/** 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/vendor/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 pendingCost/pendingInputVatAmount fields are non-null exactly when a cost push is awaiting reviewer approval — poll these, or subscribe to vendor.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 CancelledPacked 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/vendor/orders, subscribe to push notifications for two event types:

Event type Fired when
vendor.order.assigned A fulfilment group is newly assigned to you (at order creation, or a later manual/auto sourcing decision).
vendor.offer.review-decision One of your offers is approved, rejected, or its pending cost change is approved/rejected.

Payloads are deliberately minimal — an assignment notification carries only the group id, the order’s canonical id, and your vendor id; fetch full shipping/line details via GET /api/v1/vendor/orders/{groupId}. See Webhooks & HMAC verification for the full envelope shape, the signature scheme, a verified code sample, and retry/backoff behavior.

  • No credential management on THIS API. There is no create/rotate/deactivate-credential endpoint under /api/v1/vendor — that’s a permanent design choice, not a gap (mirrors the partner API’s own posture: a live API token should never be able to mint or manage the credential that issued it). Credential management for a Vendor-typed user DOES already exist, self-service, on the Linra Omni Portal dashboard (its own separate login, not this API) — see §11 Onboarding.
  • No offer-review-decision webhook without the Orders capability. An Offers-only credential cannot subscribe to vendor.offer.review-decision and must poll GET /api/v1/vendor/offers instead — a per-event-type capability split is a tracked future item if offer-only integrations turn out to be common.
  • 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.

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 (/api/v1/vendor/**) 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, 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, generate additional ones self-service under My Vendor API Credentials on the Portal dashboard — e.g. a second credential for a different environment, or to rotate a compromised one (generate new, then deactivate the old — the same zero-downtime pattern the partner API’s own credential guidance describes). 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.order.assigned (§8) or set up polling with changedSince (§7), and use POST /api/v1/vendor/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/vendor) — 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.