Developer guide

Custom store / API

How your own commerce backend — a custom storefront, a booking system, a subscription stack, an ERP export — feeds LinkNest Commerce. LinkNest issues the credentials; your developer pushes signed events, imports history in batches, and can expose an export endpoint LinkNest pulls from to repair anything a push missed.

Contract v1.0. Machine-readable schemas and the OpenAPI document ship in the repository under contracts/custom_commerce/v1/; the full guide is docs/CUSTOM_STORE_API.md.

1. Credentials

Connecting Custom store / API under Integrations mints an inbound key (in every URL) and a signing secret (shown once; rotatable from the connection page). LinkNest never reads your storefront URL and never calls your systems unless you configure an export endpoint with a token you issue.

2. Signing a request

Every request carries two headers; requests older than five minutes are refused.

X-LinkNest-Timestamp: <unix seconds>
X-LinkNest-Signature: t=<unix seconds>,v1=<hex>

v1 = HMAC-SHA256(signing_secret, "<timestamp>." + raw_body)
import hashlib, hmac, json, time, httpx

def sign(secret: str, body: bytes) -> dict:
    ts = int(time.time())
    digest = hmac.new(secret.encode(), f"{ts}.".encode() + body, hashlib.sha256).hexdigest()
    return {"Content-Type": "application/json",
            "X-LinkNest-Timestamp": str(ts),
            "X-LinkNest-Signature": f"t={ts},v1={digest}"}

body = json.dumps(event, separators=(",", ":")).encode()
httpx.post(f"{BASE}/ingest/custom/{INBOUND_KEY}/events", content=body, headers=sign(SECRET, body))

3. The event envelope

{
  "id": "evt_01HZX…",          // unique per connection — the idempotency key
  "type": "order.updated",
  "occurred_at": "2026-10-01T14:03:11Z",
  "source_version": 17,        // optional; older copies are ignored
  "data": { …entity snapshot… }
}

Every event is a full snapshot, never a diff. Re-sending an id is answered duplicate; an older copy never overwrites a newer one. Replay your outbox freely.

typedataneeds capability
order.created / order.updated / order.cancelledOrder (full snapshot)orders.read
order.deletedTombstone {id}orders.read
payment.updatedPaymentpayments.read
refund.updatedRefundrefunds.read
fulfillment.updatedShipment or service obligationfulfillment.read
customer.updated / customer.deletedCustomer / tombstonecustomers.read
product.updated / product.deletedProduct with variants / tombstonecatalog.read
inventory.updatedStock level at a locationinventory.read
location.updatedLocationlocations.read

The merchant declares which capabilities the connection provides. A service business can leave stock, locations and the catalogue out; an event for an undeclared capability is rejected at the door with the reason.

4. Money

  • Amounts are decimal strings in the order's currency. totals.total is what the customer was charged.
  • A refund is its own record with a status. Only succeeded refunds count as cash returned;pending and failed refunds are kept and reported separately. LinkNest never infers a refund from an order total going down.
  • totals.current (optional) is the order's value after edits. If it is lower than total minus succeeded refunds, the difference is reported as an adjustment, not a refund.
  • Payments carry their own status and a fee only when your processor reported one.

5. Endpoints

POST /webhooks/providers/custom_api/{key}One event, webhook style
POST /ingest/custom/{key}/eventsOne event or a batch of up to 200 with your own cursor; each event answered accepted / duplicate / rejected
POST /ingest/custom/{key}/validateSame body, nothing stored
GET /ingest/custom/{key}/deliveriesDelivery status by event id: accepted → processed or error
GET /ingest/custom/{key}/import-stateLast accepted cursor, so an interrupted import resumes

6. Missed-event repair (optional)

A webhook URL alone cannot recover history. Expose an endpoint LinkNest can pull from and enter it with a bearer token on the connection page:

GET {export_url}?since=<ISO-8601>&cursor=<opaque>&limit=200
Authorization: Bearer <export_token>
→ {"events": [ …same envelopes… ], "next_cursor": "…" | null}

LinkNest pulls hourly (15 minutes on request), applies what it had not seen, and the connection page shows the run. The URL must be public HTTPS; redirects are not followed.

Questions? Contact us. A reference backend that runs the whole route is scripts/demo_custom_store.py in the repository.