Integration journey
One continuous path from onboarding to a fulfilled order — every step, what to call, what comes back, and how it fails.
This is the path a real vendor integration follows, start to finish. Each step names the plain thing you’re doing, the exact call, a real example response, where to go next, and the failure modes you’ll actually hit — not the full reference (that’s the API Reference), just enough to keep moving. For the complete contract behind every step, see the Vendor Integration Standard.
1. Onboarding
Section titled “1. Onboarding”There’s no self-service signup on this API — a vendor relationship starts commercially, off-API. Contact Linra to begin onboarding (catalog-side setup, commercial terms). Your first credential is always issued by Linra during that process; self-service credential generation has no baseline to work from until you hold at least one active credential, and can never grant a capability beyond what your existing credentials already have.
Next: get your credentials and see what they can do.
2. Credentials + capabilities
Section titled “2. Credentials + capabilities”Once onboarding hands you a first credential, you’ll have:
Client ID: vnd_9f8c2a1b4e6d4a7c9b0e3f2d1c8a5b6eClient Secret: ••••••••••••••••••••••••••••••••••••• (shown once — copy it now)Environment: StagingCapabilities: Offers, StockEvery credential is scoped to a fixed set of capabilities — Offers, Cost, Stock,
Orders, Account, any combination. A token only lets you do what its capabilities actually
grant; calling an endpoint your token doesn’t cover is a 403, not a partial response. Further
credential management (a second environment, a replacement after rotating a compromised secret —
but never a NEW capability beyond what you already hold) is self-service — directly on this API
via POST /api/v1/credentials (requires Account), or on the Linra Omni Portal dashboard. See
Authentication & capabilities for the full model.
Get a token:
curl -X POST https://api-omni-vendors-stg.linra.net/api/v1/auth/token \ -H "Content-Type: application/json" \ -d '{"clientId": "vnd_9f8c...", "clientSecret": "your-secret"}'const res = await fetch('https://api-omni-vendors-stg.linra.net/api/v1/auth/token', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ clientId: CLIENT_ID, clientSecret: CLIENT_SECRET }),});const { payload } = await res.json();// payload.accessToken, payload.expiresIn (seconds — always 300 today){ "state": "SUCCESS", "payload": { "accessToken": "eyJhbGciOi...", "tokenType": "Bearer", "expiresIn": 300 } }Fails as: 401 UNAUTHORIZED_INVALID_CREDENTIALS (wrong id/secret) or 401 UNAUTHORIZED_VENDOR_INACTIVE; 429 past 10 requests/60s. The token is valid for 5
minutes (an expired token is rejected within at most 30 seconds of clock skew, with
401 UNAUTHORIZED_TOKEN_EXPIRED) — cache it and refresh proactively.
Next: submit your first offer.
3. Submit offers
Section titled “3. Submit offers”One door for two entry paths: supply variantId to propose a match to an existing catalog
item, or rawBrandName/rawProductName for a free-form entry a reviewer will canonicalize later.
Either way, nothing goes live until an internal reviewer approves it — this is the SAME review
queue the Portal vendor UI uses.
curl -X POST https://api-omni-vendors-stg.linra.net/api/v1/scent/offers \ -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d '{ "rawBrandName": "Example Brand", "rawProductName": "Example EDP 100ml", "cost": 145.00, "costIsVatInclusive": false, "declaredInStock": true, "declaredStockQuantity": 25 }'{ "state": "SUCCESS", "payload": { "id": "offer-guid", "status": "PendingMatch", "cost": { "amount": 145.0, "isVatInclusive": false, "enteredVatInclusive": false, "inputVat": 0, "pending": null }, "stock": { "declaredInStock": true, "declaredQuantity": 25 } }}status starts at PendingMatch (no variantId supplied — awaiting a reviewer to canonicalize
it) or PendingReview (you proposed a match) — both mean “your submission is being reviewed,”
though you’ll see the two distinct strings in the response. Set vendorExternalId on create and a
retried submission with the SAME value returns the original offer unchanged instead of creating a
duplicate — a safe retry, no separate idempotency key needed.
Fails as: 400 (missing rawBrandName/rawProductName when variantId is omitted), 403 FORBIDDEN_CAPABILITY_NOT_GRANTED (your token doesn’t hold Offers).
Next: keep cost and stock current.
4. Manage stock/cost
Section titled “4. Manage stock/cost”Stock applies instantly, no review gate — it’s a real-world fact, not a commercial term.
It’s a sub-resource of the offer (PATCH .../offers/{offerId}/stock, not a separate top-level
/stock resource):
curl -X PATCH https://api-omni-vendors-stg.linra.net/api/v1/scent/offers/offer-guid/stock \ -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d '{"declaredInStock": true, "declaredStockQuantity": 40}'declaredInStock is required — a body without it returns 400 VALIDATION_ERRORS and the offer is
left unchanged; false with a quantity above 0, or true with a quantity of 0, is rejected the same way.
Cost always routes through the approval pipeline, independently of the Offers capability (a
Cost-only credential can push cost without ever touching offer identity):
curl -X PATCH https://api-omni-vendors-stg.linra.net/api/v1/scent/offers/offer-guid/cost \ -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d '{"cost": 150.00, "costIsVatInclusive": false}'{ "state": "SUCCESS", "payload": { "id": "offer-guid", "status": "Approved", "cost": { "amount": 145.0, "isVatInclusive": false, "enteredVatInclusive": false, "inputVat": 0, "pending": { "amount": 150.0, "inputVat": null } } }}On an already-Approved offer, a cost push lands in cost.pending — the LIVE cost.amount stays
untouched until a reviewer applies it. On an offer never yet approved, cost just updates directly
(nothing is live regardless, until approval). cost.isVatInclusive on every RESPONSE from this API
is always false — pair it with the unchanged cost.amount value on a later write to safely
resubmit without re-interpreting an already-net figure as a fresh gross one;
cost.enteredVatInclusive (response-only, never accepted on a write) shows you how the cost was
ACTUALLY entered. Both pushes are idempotent — re-sending the identical value is a no-op, never a
duplicate side effect.
Fails as: 403 FORBIDDEN_CAPABILITY_NOT_GRANTED (missing Stock/Cost respectively), 404
(an offer id that isn’t yours, or was entered manually by a Linra catalog admin on your behalf —
still yours by ownership, but only reachable through this specific door if you originally submitted
it via the API).
Next: fulfil what gets assigned to you.
5. Receive orders
Section titled “5. Receive orders”curl https://api-omni-vendors-stg.linra.net/api/v1/scent/orders?pageSize=20 \ -H "Authorization: Bearer $TOKEN"{ "state": "SUCCESS", "payload": { "items": [ { "groupId": "group-guid", "status": "Pending", "shipTo": { "recipient": "Jane Doe", "line1": "123 King Fahd Rd", "city": "Riyadh", "country": "SA", "phone": "+9665..." }, "tracking": { "ref": null, "carrier": null, "carrierNameOther": null }, "lines": [{ "lineId": "line-guid", "variantId": "1a2b...", "sku": "EX-EDP-100", "quantity": 2 }] } ], "pagination": { "page": 1, "pageSize": 20, "totalCount": 1, "totalPages": 1 } }}Each row is a fulfilment group — the shipment/parcel assigned to you specifically (a single
order can split across several vendors’ groups, or Linra’s own Self-fulfilled group; you only ever
see your own). Called groupId on every vendor-facing endpoint and route ({groupId} in the URL
above) — your own correlation reference for this shipment; this response never carries the
reselling partner’s own order identifier at all. (If you’ve also read the partner developer docs:
the SAME shipment concept is called fulfillmentGroupId there, e.g. on a return request — same id,
different field name depending on which side of the API you’re on.) You’ll
never see the reselling partner’s identity, order total, commission, or any money field on this
surface — you’re paid via your own commercial terms with Linra, tracked entirely on the offer/cost
side above, never derived from what a partner paid. status accepts a repeatable filter
(?status=Shipped&status=Delivered); set changedSince to poll only groups that changed since
your last check rather than re-fetching everything.
Fails as: 403 FORBIDDEN_CAPABILITY_NOT_GRANTED (missing Orders).
Next: tell Linra when you’ve shipped it.
6. Push fulfilment status
Section titled “6. Push fulfilment status”curl -X PATCH https://api-omni-vendors-stg.linra.net/api/v1/scent/orders/group-guid/status \ -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d '{"status": "Shipped", "trackingRef": "ARAMEX-123456789", "carrier": "Aramex"}'{ "state": "SUCCESS", "payload": { "applied": true, "group": { "groupId": "group-guid", "status": "Shipped", "tracking": { "ref": "ARAMEX-123456789", "carrier": "Aramex", "carrierNameOther": null } } } }You may only push Shipped, Delivered, or Cancelled — forward-only. Packed is an internal
operator-only transition (Linra confirms packing before you ship), and any backward, stale, or
invalid target is a clean no-op: 200 OK, applied: false, the group’s state unchanged — never
an error, so your automation can tell “my push landed” apart from “this group already moved past
that point” without special-casing an exception. Re-pushing the identical, already-applied status
is the same safe no-op.
Fails as: 403 FORBIDDEN_CAPABILITY_NOT_GRANTED, 404 (a group that isn’t yours). A rejected
status VALUE (not one of the three allowed) is 400.
Next: stop polling — subscribe to push notifications.
7. Webhooks
Section titled “7. Webhooks”curl -X POST https://api-omni-vendors-stg.linra.net/api/v1/scent/webhooks \ -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d '{"url": "https://your-integration.example.com/webhooks/linra", "eventTypes": ["vendor.scent.order.assigned", "vendor.scent.offer.review-decision"]}'{ "state": "SUCCESS", "payload": { "subscription": { "id": "sub-guid", "url": "https://your-integration.example.com/webhooks/linra", "eventTypes": ["vendor.scent.order.assigned", "vendor.scent.offer.review-decision"], "isActive": true }, "secret": "whsec_5f8a2e1c9b3d4a6f8e0c2b1a7d9e3f4c" }}The secret is shown exactly once — store it immediately, it’s your HMAC signing key
(subscription carries no vendorId — redundant on a self-only surface). The example above
subscribes to both event types at once, which needs a credential holding both Orders and
Offers. The two event types have two distinct capability owners: subscribing to
vendor.scent.order.assigned requires Orders; subscribing to vendor.scent.offer.review-decision
requires Offers — checked per event type you list, not once for the whole call. An Offers-only
credential can subscribe to its own offer-review notifications (drop vendor.scent.order.assigned
from eventTypes) instead of polling step 3; it only gets 403 FORBIDDEN_CAPABILITY_NOT_GRANTED
if it tries to include vendor.scent.order.assigned too. Every OTHER webhook action requires no
capability at all — ownership of the subscription is the only gate. Full verified
signature-checking code (Node.js) lives in
Webhooks & HMAC verification — copy that sample rather than hand-rolling
verification from scratch.
Fails as: up to 5 active subscriptions per vendor; a 6th create is 422 BUSINESS_VENDOR_WEBHOOK_SUBSCRIPTION_LIMIT — rotate or delete one first.
You’ve now covered the full loop: onboarding → credentials → offer → stock/cost → assigned order → status push → webhook. From here, the Vendor Integration Standard is the canonical reference for every rule mentioned above, the API Reference has the complete wire shape for every field, and the guides in the sidebar go deep on any one step.

