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.
| type | data | needs capability |
|---|---|---|
| order.created / order.updated / order.cancelled | Order (full snapshot) | orders.read |
| order.deleted | Tombstone {id} | orders.read |
| payment.updated | Payment | payments.read |
| refund.updated | Refund | refunds.read |
| fulfillment.updated | Shipment or service obligation | fulfillment.read |
| customer.updated / customer.deleted | Customer / tombstone | customers.read |
| product.updated / product.deleted | Product with variants / tombstone | catalog.read |
| inventory.updated | Stock level at a location | inventory.read |
| location.updated | Location | locations.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.totalis what the customer was charged. - A refund is its own record with a status. Only
succeededrefunds count as cash returned;pendingandfailedrefunds 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}/events | One event or a batch of up to 200 with your own cursor; each event answered accepted / duplicate / rejected |
| POST /ingest/custom/{key}/validate | Same body, nothing stored |
| GET /ingest/custom/{key}/deliveries | Delivery status by event id: accepted → processed or error |
| GET /ingest/custom/{key}/import-state | Last 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.