Skip to content

Retailer API ​

Retailers who don't run a Shopify store — or who'd rather drive everything from their own system — connect to LGL through the Retailer API: you browse our catalog, check stock, send us orders, and collect tracking when they ship.

It's the same fulfillment program as the Shopify app, delivered as a plain REST API. Everything the app does for a Shopify store, you do with HTTP calls.

Full reference

This page is the orientation. The complete interactive reference — every endpoint, parameter, schema and example, with a console to try calls against your own key — lives at api.livegoodlogistics.com/retailer/docs.

Authentication ​

Contact LGL to receive your API keys, then send one on every request:

bash
curl "https://api.livegoodlogistics.com/retailer/products" \
  -H "X-API-Key: your_api_key_here"

You get two:

KeyPrefixWhat it is
Sandboxsr_test_A real retailer identity that drives the real pipeline, so you're testing the system that will actually serve you. Orders you create are tagged internally and cancelled automatically overnight, so nothing ships and nothing is invoiced.
Productionsr_live_Live. Real orders, really shipped, really billed.

Nothing in the API branches on which key you send, so you write your integration once and swap the key when you're ready to go live.

Every endpoint except /health needs the key. Rate limit is 60 requests per minute.

SKUs: use ours, exactly ​

This is the most common integration mistake

Every sku you send us — in an order, in an inventory lookup — must be the LGL SKU, character for character. We do not translate, strip prefixes or suffixes, or fuzzy-match. If you keep our items under your own codes (or ours with something appended), map them back before you call us.

A SKU we don't recognise comes back as resolved: false on that line. If no line resolves, the whole order is rejected with NO_VALID_ITEMS — better than shipping you a surprise.

resolved: false always means we looked and didn't find it. If we can't reach our own catalog to look at all, you get 503 LOOKUP_UNAVAILABLE and no order is created — we won't guess, create a short order, and tell you a SKU is missing when we never checked. Retry the identical request; Retry-After suggests how long to wait.

Use GET /products?sku=… to confirm a mapping. It's an exact match and returns the product owning that SKU, along with all of its variants — which is usually the fastest way to build your mapping table in the first place.

The order workflow ​

StepEndpointWhat it does
1GET /productsBrowse the catalog
2GET /inventory?all=trueFull stock sync (or ?skus=X,Y for specific SKUs)
3POST /ordersSend us an order to fulfill
4GET /orders/changes?since=…Poll one feed for everything that shipped, with tracking

Steps 2 and 4 both have delta versions so you're not re-pulling the world: GET /inventory/changes?since=… returns only SKUs whose stock moved, and GET /orders/changes?since=… only orders that changed. Both hand you a next_since to use on your next call.

Finding products ​

GET /products pages the catalog. Beyond search (a fuzzy match across title, SKU and brand) you can narrow and order the results:

bash
# Everything added since August, newest first, in stock only
curl "https://api.livegoodlogistics.com/retailer/products?created_after=2026-08-01&in_stock=true&sort=created_at" \
  -H "X-API-Key: your_api_key_here"
ParameterWhat it does
sort + ordercreated_at, updated_at, title, inventory, vendor, product_type. Dates and inventory default to newest/most first; names default to A–Z.
created_after / updated_afterYYYY-MM-DD or a full ISO timestamp
skuExact SKU (see above)
in_stockOnly what we can actually ship today
product_typePlant or Accessory
category / subcategoryOur catalog taxonomy — GET /categories lists them

Short pages are normal

Products we don't offer for resale are removed after a page is read, so a page can come back with fewer items than you asked for — sometimes zero — while more results still follow. Page until has_next_page is false; never stop on a short page.

What's new ​

There's a catch to finding new items, and it's worth understanding: a product's created_at is the date the product was added. When we add a new size to a plant we've carried for years, that's a new item to you — but it inherits the original product's date and won't appear at the top of ?sort=created_at. For our catalog that's most of what "new" means.

So there's a second endpoint that reports each variant's own date:

bash
curl "https://api.livegoodlogistics.com/retailer/variants?created_after=2026-08-01" \
  -H "X-API-Key: your_api_key_here"

Every row carries its parent product plus an isNewProduct flag — true when the whole product is new, false when it's a new size on something you may already carry. Pass include_new_products=false to see only the latter. The window defaults to the last 90 days.

Inventory ​

GET /inventory?all=true gives you the whole catalog's availability in one call. GET /inventory?skus=A,B,C checks up to 200 specific SKUs. A SKU we don't know comes back with found: false rather than an error, so a mapping gap shows up as data instead of a failed sync.

Inventory covers exactly the SKUs GET /products returns — our live catalog. Products we haven't launched yet, and stock we hold for other partners, are left out of every inventory response (and of inventory.updated webhooks), and a lookup of one reads found: false. When a product goes live it appears in both, so a periodic ?all=true sync picks it up.

Once you've done a full sync, poll the delta instead:

bash
curl "https://api.livegoodlogistics.com/retailer/inventory/changes?since=2026-09-21T00:00:00Z" \
  -H "X-API-Key: your_api_key_here"

Our stock moves throughout the day as orders ship. Pulling changes every few minutes keeps you close without hammering the full feed.

Sending an order ​

bash
curl -X POST "https://api.livegoodlogistics.com/retailer/orders" \
  -H "X-API-Key: your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "reference_number": "YOUR-ORDER-1234",
    "shipping_address": {
      "first_name": "Jane", "last_name": "Doe",
      "address1": "123 Main St", "city": "Charlotte",
      "province": "NC", "zip": "28217", "country": "US"
    },
    "line_items": [{ "sku": "EDB-FIG-CHI-NA-1G", "quantity": 2 }]
  }'

reference_number is your order number and it's how we de-duplicate. Send the same one twice and the second call returns 409 DUPLICATE_ORDER with the existing order attached — safe to retry a request you're not sure landed.

The 201 response tells you exactly what we did: each line comes back with resolved, the price we'll bill, and bundled_price. Check it rather than assuming — a line that didn't resolve is right there in the response.

Scheduling a ship date ​

Send ship_date (or shipping_date inside custom_attributes) as YYYY-MM-DD, YYYY-MM-DD HH:MM:SS, or a full ISO timestamp, and we hold the order until then.

A date we can't read is an error, never a silent "ship today"

If we can't parse what you send, you get 400 INVALID_SHIP_DATE and no order is created. And the 201 response echoes back the ship_date we recorded — null means none was requested. Compare it to what you sent.

Anything else you put in custom_attributes rides through to our warehouse untouched, so your own references and handling notes reach the people packing the box. Top-level fields we don't recognise are listed back to you as ignored_fields rather than being quietly dropped.

What you're billed ​

Each line is priced at your agreed cost. Accessories bill at a lower bundled rate when they ship in the same box as a plant from the same location — the bundled_price flag on each line tells you which rate applied. Nothing about pricing depends on what you send us beyond the SKU and quantity. If we've agreed account-specific prices with you, those SKUs bill at your agreed price as-is — the bundled accessory rate doesn't apply on top of it.

Some SKUs are bundles, such as a 2-pack or a tree with its planter, that we ship as their separate parts. The line then also lists the components we'll ship. The bundle's price is unchanged and is split across the parts. Parts can ship in separate boxes, so expect a tracking number for each box.

Getting tracking back ​

Two ways, and they work well together.

Poll the one feed that covers everything:

bash
curl "https://api.livegoodlogistics.com/retailer/orders/changes?since=2026-09-21T00:00:00Z" \
  -H "X-API-Key: your_api_key_here"

Each row carries the status, tracking number, carrier and tracking URL. Advance since to the returned next_since and call again.

Or register a webhook and we'll push instead:

bash
curl -X PUT "https://api.livegoodlogistics.com/retailer/webhook" \
  -H "X-API-Key: your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://your-app.example.com/lgl-webhooks"}'

The response carries your signing secret — shown only once, so store it before you close the terminal.

EventWhen
order.fulfilledAn order shipped — includes the tracking details
inventory.updatedStock changed on a SKU

Every delivery carries X-LGL-Signature (HMAC-SHA256 of the raw body with your signing secret) so you can verify it's really us, and X-LGL-Event-Id, which is stable across retries — dedupe on it. Sign over the raw bytes; re-serializing the JSON changes them and the comparison fails.

GET /webhook shows your current config, POST /webhook/rotate-secret issues a new secret, DELETE /webhook turns it off, and GET /webhook/deliveries shows recent attempts — including the response your endpoint gave us, which is usually enough to debug a failing receiver without involving us.

Keep the poll as a backstop

Answer 2xx within 10 seconds. We retry up to three times, then stop — so webhooks are the fast path, not a guarantee. Keep a periodic GET /orders/changes sweep running; that endpoint is always the source of truth.

Good to know ​

  • Build against the sandbox key first. It runs the real pipeline end to end, so anything that would break in production breaks there instead — and the orders clean themselves up overnight.
  • State restrictions still apply. Some plants can't legally ship to certain states. If you sell from our catalog, check the restriction data on the product before you take the customer's money — see State Restrictions.
  • Addresses can be corrected right up until we fulfill. Once an order has shipped it's locked.
  • GET /health needs no key — handy for a monitoring check.
  • Billing works the same as for every retailer: a weekly statement of what shipped, at your agreed cost.

The API is actively evolving — if your integration needs something it doesn't do yet, talk to us: [email protected].

LGL Order and Inventory Sync is provided by Live Good Logistics for authorized retail partners.