Skip to content

Integration journey

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.

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.

Once onboarding hands you a first credential, you’ll have:

Client ID: vnd_9f8c2a1b4e6d4a7c9b0e3f2d1c8a5b6e
Client Secret: ••••••••••••••••••••••••••••••••••••• (shown once — copy it now)
Environment: Staging
Capabilities: Offers, Stock

Every credential is scoped to a fixed set of capabilitiesOffers, Cost, Stock, Orders, 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 on the Linra Omni Portal dashboard, a separate surface from this API. See Authentication & capabilities for the full model.

Get a token:

Terminal window
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). The token is valid for 5 minutes — cache it and refresh proactively.

Next: submit your first offer.

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.

Terminal window
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": 145.0,
"costIsVatInclusive": false,
"costEnteredVatInclusive": false,
"declaredInStock": true,
"declaredStockQuantity": 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.

Stock applies instantly, no review gate — it’s a real-world fact, not a commercial term:

Terminal window
curl -X PATCH https://api-omni-vendors-stg.linra.net/api/v1/scent/stock/offer-guid \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"declaredInStock": true, "declaredStockQuantity": 40}'

Cost always routes through the approval pipeline, independently of the Offers capability (a Cost-only credential can push cost without ever touching offer identity):

Terminal window
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": 145.0,
"pendingCost": 150.0,
"costIsVatInclusive": false,
"costEnteredVatInclusive": false
}
}

On an already-Approved offer, a cost push lands in pendingCost — the LIVE cost stays untouched until a reviewer applies it. On an offer never yet approved, cost just updates directly (nothing is live regardless, until approval). costIsVatInclusive on every RESPONSE from this API is always false — pair it with the unchanged cost value on a later write to safely resubmit without re-interpreting an already-net figure as a fresh gross one; costEnteredVatInclusive (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.

Terminal window
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",
"orderGlobalId": "ORD-2026-000123",
"status": "Pending",
"shipRecipient": "Jane Doe",
"shipCity": "Riyadh",
"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) — think of it as the shipment id. (If you’ve also read the partner developer docs: the SAME 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.) orderGlobalId is Linra’s own canonical order id, present purely for support correlation; 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. 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.

Terminal window
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"}'
{ "state": "SUCCESS", "payload": { "applied": true, "group": { "groupId": "group-guid", "status": "Shipped", "trackingRef": "ARAMEX-123456789" } } }

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.

Terminal window
curl -X POST https://api-omni-vendors-stg.linra.net/api/v1/webhooks \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"url": "https://your-integration.example.com/webhooks/linra", "eventTypes": ["vendor.order.assigned", "vendor.offer.review-decision"]}'
{
"state": "SUCCESS",
"payload": {
"subscription": { "id": "sub-guid", "url": "https://your-integration.example.com/webhooks/linra", "eventTypes": ["vendor.order.assigned", "vendor.offer.review-decision"], "isActive": true },
"secret": "whsec_5f8a2e1c9b3d4a6f8e0c2b1a7d9e3f4c"
}
}

The secret is shown exactly once — store it immediately, it’s your HMAC signing key. 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.order.assigned requires Orders; subscribing to vendor.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.order.assigned from eventTypes) instead of polling step 3; it only gets 403 FORBIDDEN_CAPABILITY_NOT_GRANTED if it tries to include vendor.order.assigned too. Full verified signature-checking code (Node.js, tested against a real delivery) 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.