Skip to main content

Shopify API Reference

All public store endpoints are unauthenticated — L402 payment proof serves as authorization. Admin endpoints require your Lightning Enable API key.

Base URL: https://api.lightningenable.com


Public Endpoints

These endpoints are accessible without an API key. The {slug} parameter is the URL-safe identifier you chose when creating the integration.

Get Catalog

Fetches the product catalog from your Shopify store. Results are cached.

GET /api/shopify/{slug}/catalog

Response: 200 OK

{
"storeName": "your-store.com",
"slug": "your-store",
"shipping": {
"domesticUsd": 5.99,
"domesticOnly": true,
"freeShippingEnabled": true,
"freeShippingThresholdUsd": 50.00
},
"products": [
{
"productId": 8234567890123,
"title": "Premium Coffee Beans",
"description": "Single-origin Ethiopian coffee, light roast",
"imageUrl": "https://cdn.shopify.com/s/files/...",
"variants": [
{
"variantId": 44567890123456,
"title": "12oz Bag",
"priceUsd": 18.99,
"available": true
},
{
"variantId": 44567890123457,
"title": "2lb Bag",
"priceUsd": 34.99,
"available": true
}
]
}
]
}

Error Responses:

StatusMeaning
404Integration not found or inactive

Create Checkout

Creates an L402 checkout with a Lightning invoice. Returns HTTP 402 with the payment challenge.

POST /api/shopify/{slug}/checkout

Headers (required):

HeaderFormatDescription
X-Buyer-Location{country}-{state}-{zip}Required. The buyer's location for destination-based tax calculation (e.g., US-FL-34787). There is no fallback — a missing or blank header returns 400. Pass the buyer's actual country-state-zip, never the seller's.

Request Body:

{
"items": [
{ "variantId": 44567890123456, "quantity": 2 },
{ "variantId": 44567890123457, "quantity": 1 }
],
"email": "customer@example.com"
}
FieldRequiredDescription
itemsYesArray of { variantId, quantity } (1–10 items)
emailNoBuyer email captured at checkout time. When set and the merchant's payment provider fires a paid webhook (Strike, OpenNode), the buyer is automatically sent the claim URL once the payment lands. This is the recovery channel when the buyer's wallet returns no preimage — e.g., Strike-to-Strike payments, where the agent can't complete the L402 retry below. Agents buying on behalf of a human should collect and pass this; agents acting alone can omit it and rely on the L402 retry path.

Validation Rules:

  • 1–10 items per checkout
  • Each item quantity: 1–10
  • Total quantity across all items: max 10
  • All variant IDs must exist in the catalog
  • All variants must be available (in stock)

Response: 402 Payment Required

The response includes a WWW-Authenticate header with the L402 challenge:

WWW-Authenticate: L402 macaroon="...", invoice="lnbc..."

Response Body:

{
"orderId": "shpfy_a1b2c3d4e5f6",
"items": [
{
"variantId": 44567890123456,
"productTitle": "Premium Coffee Beans",
"variantTitle": "12oz Bag",
"quantity": 2,
"priceUsd": 18.99
},
{
"variantId": 44567890123457,
"productTitle": "Premium Coffee Beans",
"variantTitle": "2lb Bag",
"quantity": 1,
"priceUsd": 34.99
}
],
"subtotalUsd": 72.97,
"shippingUsd": 0.00,
"taxUsd": 5.11,
"taxNote": null,
"totalUsd": 78.08,
"totalSats": 77200,
"claimExpiresAt": "2026-03-05T18:30:00Z",
"invoice": "lnbc721500n1pn...",
"macaroonBase64": "AgELbGlnaHRuaW5n...",
"paymentHash": "a1b2c3d4e5f6...",
"invoiceExpiresAt": "2026-02-26T18:40:00Z"
}

Key Fields:

  • invoice — BOLT11 Lightning invoice to pay
  • macaroonBase64 — L402 macaroon (save this for the post-payment retry and claim steps)
  • paymentHash — links the macaroon to the invoice
  • claimExpiresAt — deadline for claiming the order after payment (configurable window, default 30 days)
  • totalSats — BTC amount locked at current exchange rate
  • taxUsd — tax amount calculated via Shopify's Draft Order API from the buyer's X-Buyer-Location
  • taxNote — always null. (Tax is always computed from the buyer's location; there is no estimation path, so there is nothing to annotate.)
  • totalUsd — total including subtotal + shipping + tax
No claim token in the 402 response — by design

The checkout response deliberately does not include a claim token or claim page URL. They are credentials for the post-payment claim flow — emitting them before payment would let an observer race the legitimate buyer to claim the order once the payment lands. To obtain them, prove payment via the checkout retry below.

Tax Calculation:

Tax is calculated at checkout using Shopify's Draft Order API. A temporary draft order is created with the cart items and a partial address (derived from the buyer location), Shopify computes the tax, the amount is read, and the draft is deleted. The tax is included in the Lightning invoice total.

The buyer location comes only from the X-Buyer-Location request header (format: {country}-{state}-{zip}, e.g., US-FL-34787). The header is required — there is no fallback to a stored default, because computing tax for the wrong jurisdiction (e.g., the seller's address) would be incorrect. If the header is missing or blank, checkout returns a 400 error.

Error Responses:

StatusMeaning
400Invalid cart (bad variant ID, unavailable item, exceeds limits), missing/blank X-Buyer-Location, or a payment credential for a disabled protocol
404Integration not found
503Bitcoin price feed temporarily unavailable — all price sources failed, so no invoice is issued at a stale rate. Response includes a correlationId. Retryable — retry shortly.
503Tax calculation failed (Shopify Draft Order API error or missing Admin API token). Retryable.

Complete a Paid Checkout (Checkout Retry)

After paying the invoice, re-POST the same checkout endpoint with your L402 payment proof in the Authorization header. This is the standard L402 client pattern (retry the same URL after payment) and it's how you obtain the claim credentials that are deliberately withheld from the 402 response.

POST /api/shopify/{slug}/checkout
Authorization: L402 {macaroonBase64}:{preimageHex}

Re-send the same body you sent on the first checkout. The body must be valid (1–10 items) — [ApiController] model validation runs before the credential is inspected, so an empty {"items": []} is rejected with 400 even with a valid Authorization header. Its content does not affect the retry, though: the payment credential identifies the paid order (by macaroon, or by SHA256(preimage) == paymentHash when no macaroon is sent). The simplest approach is to keep the original checkout body in a variable and reuse it.

Response: 200 OK

{
"orderId": "shpfy_a1b2c3d4e5f6",
"status": "PaidAwaitingDetails",
"shopifyOrderNumber": null,
"claimPageUrl": "https://your-store.com/pages/claim-your-order?token=SC-x7k9m2p4",
"message": "Payment verified. Submit shipping details to complete your order."
}
  • claimPageUrl — the merchant's claim page with the claim token in the ?token= query parameter. Present only when the merchant has configured a claim page; otherwise null. Hand this URL to the human buyer, or extract the token to call Claim Order directly.
  • If the buyer's wallet doesn't return a preimage (e.g., some Strike-to-Strike payments), this retry isn't possible — that's what the optional email field at checkout is for: the claim URL is sent to the buyer automatically once the payment lands.

Error Responses:

StatusMeaning
400Invalid request body (empty/items: [] or more than 10 items — validated before the credential), credential for a disabled protocol, or invalid preimage format
401L402 verification failed (macaroon/preimage don't verify)
404No order found for this payment token

Claim Order

Claims an order after payment: verifies payment, saves shipping details, and creates a Shopify order.

POST /api/shopify/{slug}/claim

Headers:

Authorization: L402 {macaroonBase64}:{preimageHex}
Content-Type: application/json

The Authorization header format is L402 <macaroon>:<preimage>:

  • macaroon — the macaroonBase64 from the checkout response
  • preimage — the 64-character hex preimage obtained after paying the invoice

When is the Authorization header required? Only for orders still in PendingPayment — the preimage is what proves payment and transitions the order to paid. For orders already marked paid (PaidAwaitingDetails, e.g. after the checkout retry or a provider webhook), the claim token alone is sufficient — this is what lets the merchant's claim page work from a plain browser without L402 headers.

Request Body:

{
"claimToken": "SC-x7k9m2p4",
"email": "customer@example.com",
"shippingAddress": {
"firstName": "Jane",
"lastName": "Doe",
"address1": "123 Main St",
"address2": "Apt 4B",
"city": "Austin",
"province": "TX",
"zip": "78701",
"country": "US",
"phone": "+15125551234"
}
}

Shipping Address Fields:

FieldRequiredMax Length
firstNameYes200
lastNameYes200
address1Yes500
address2No500
cityYes100
provinceNo100
zipNo20
countryYes100
phoneNo30

Response: 200 OK

{
"orderId": "shpfy_a1b2c3d4e5f6",
"status": "PaidWithDetails",
"shopifyOrderNumber": "#1042",
"claimPageUrl": "https://your-store.com/pages/claim-your-order?token=SC-x7k9m2p4",
"message": "Order #1042 created successfully. You'll receive shipping confirmation at customer@example.com."
}

Error Responses:

StatusMeaning
400Expired claim window, already claimed, missing payment proof, invalid preimage format, failed L402 verification, or international address on a domestic-only store
401Payment credential for a disabled protocol only (a failing macaroon/preimage returns 400, not 401)
404Unknown claim token (or token belongs to a different store)

Get Order Status

Check the status of an order. Requires the claim token as a query parameter.

GET /api/shopify/{slug}/orders/{orderId}?claimToken={claimToken}

The claim token is single-use for claiming, but it stays valid as a read credential after a successful claim — status lookups with the same token keep working for claimed orders (PaidWithDetails, Fulfilled, Shipped), so the buyer can track their order through to delivery. Claimed orders remain queryable with the claim token for 90 days after claiming, after which they are purged. For orders that were never claimed, status lookups stop working once the claim window expires.

Response: 200 OK

{
"orderId": "shpfy_a1b2c3d4e5f6",
"status": "Shipped",
"totalUsd": 72.97,
"paidSats": 72150,
"shopifyOrderNumber": "#1042",
"trackingNumber": "1Z999AA10123456784",
"trackingCarrier": "UPS",
"trackingUrl": "https://www.ups.com/track?tracknum=1Z999AA10123456784",
"createdAt": "2026-02-26T18:30:00Z",
"paidAt": "2026-02-26T18:31:15Z",
"shippedAt": "2026-02-28T14:22:00Z"
}

Error Responses:

StatusMeaning
404Order not found, claim token doesn't match, or claim window expired before the order was claimed. A token mismatch is deliberately a uniform 404 (never 401) to prevent order-ID enumeration.

Get llms.txt Manifest

Returns a plain-text llms.txt manifest for the store — tagline, L402 endpoints, products, shipping rules, and links, formatted for AI agent discovery. Merchants can redirect /llms.txt on their own domain to this endpoint.

GET  /api/shopify/{slug}/llms.txt
HEAD /api/shopify/{slug}/llms.txt

Response: 200 OKtext/plain; charset=utf-8. Cached with the same TTL as the catalog.

Error Responses:

StatusMeaning
404Integration not found or inactive (also text/plain)

Merchant Admin Endpoints

These endpoints require your Lightning Enable API key in the X-API-Key header.

Get Integration

GET /api/merchant/shopify

Returns your Shopify integration configuration. The Admin API token is never returned — only hasAdminApiToken: true/false.

Create Integration

POST /api/merchant/shopify

See Setup Guide for the full request schema.

Update Integration

PUT /api/merchant/shopify

Partial update — include only the fields you want to change.

{
"domesticShippingUsd": 6.99,
"freeShippingEnabled": true,
"defaultTaxLocation": "US-FL-34787",
"isActive": false,
"listInRegistry": true,
"registryCategories": "[\"commerce\",\"food-and-beverage\"]",
"registryDescription": "Your store description for agent discovery."
}

Invalidate Cache

POST /api/merchant/shopify/invalidate-cache

Forces a refresh of the cached product catalog on the next request.

Refresh Shop Info

POST /api/merchant/shopify/refresh-shop-info

Refreshes the integration's canonical store identity (shopifyDomain and the internal *.myshopify.com handle) from Shopify's shop.json. Use after changing your primary domain in Shopify. Returns the updated integration on success; 400 if no access token is configured; 502 if Shopify doesn't return shop info (e.g., revoked token — the response body includes the upstream status).

Get Stats

GET /api/merchant/shopify/stats

Returns checkout conversion stats for every Shopify integration you own — scoped to your merchant account. total is the roll-up; byStore has one entry per store.

Response:

{
"merchantId": 42,
"total": {
"totalIntegrations": 1,
"totalCheckouts": 120,
"paidOrders": 18,
"conversionRate": 0.15,
"totalPaidSats": 1450000,
"totalPaidUsd": 1312.50,
"fulfilled": 16,
"shipped": 14,
"firstOrderAt": "2026-03-01T10:00:00Z",
"lastOrderAt": "2026-06-30T21:45:00Z"
},
"byStore": [
{
"slug": "my-store",
"shopifyDomain": "your-store.com",
"totalCheckouts": 120,
"paidOrders": 18,
"conversionRate": 0.15,
"totalPaidSats": 1450000,
"totalPaidUsd": 1312.50,
"fulfilled": 16,
"shipped": 14,
"firstOrderAt": "2026-03-01T10:00:00Z",
"lastOrderAt": "2026-06-30T21:45:00Z"
}
]
}

List Orders

GET /api/merchant/shopify/orders
GET /api/merchant/shopify/orders?status=PaidWithDetails
GET /api/merchant/shopify/orders?page=2&pageSize=10

Query Parameters:

ParameterDefaultDescription
status(all)Filter by status: PendingPayment, PaidAwaitingDetails, PaidWithDetails, Fulfilled, Shipped
page1Page number
pageSize20Results per page

Response:

[
{
"orderId": "shpfy_a1b2c3d4e5f6",
"status": "PaidWithDetails",
"totalUsd": 72.97,
"paidSats": 72150,
"email": "customer@example.com",
"shopifyOrderNumber": "#1042",
"createdAt": "2026-02-26T18:30:00Z"
}
]

Complete Purchase Flow Example

Here's the full flow using curl:

# 1. Browse products
CATALOG=$(curl -s https://api.lightningenable.com/api/shopify/my-store/catalog)
echo "$CATALOG" | jq '.products[0].variants[0]'

# 2. Create checkout (get variant ID from catalog)
# X-Buyer-Location is REQUIRED for tax calculation — pass the buyer's country-state-zip
# email is optional but recommended when buying for a human — it's the recovery
# channel if your wallet doesn't return a preimage
# Keep the body in a variable — you re-send the SAME body on the retry (step 4).
CHECKOUT_BODY='{"items": [{"variantId": 44567890123456, "quantity": 1}], "email": "customer@example.com"}'
CHECKOUT=$(curl -s -X POST https://api.lightningenable.com/api/shopify/my-store/checkout \
-H "Content-Type: application/json" \
-H "X-Buyer-Location: US-FL-34787" \
-d "$CHECKOUT_BODY")

# Extract the invoice and macaroon (there is NO claimToken in the 402 response — by design)
INVOICE=$(echo "$CHECKOUT" | jq -r '.invoice')
MACAROON=$(echo "$CHECKOUT" | jq -r '.macaroonBase64')
ORDER_ID=$(echo "$CHECKOUT" | jq -r '.orderId')

# 3. Pay the invoice (using Lightning Enable MCP, lncli, or any Lightning wallet)
# This gives you the preimage (64 hex chars)
PREIMAGE="your_preimage_hex_here"

# 4. Retry the SAME checkout endpoint with your payment proof.
# Re-send the ORIGINAL checkout body — it must be valid (1–10 items), because
# model validation runs before the L402 credential is inspected (an empty
# {"items": []} would 400). Its content doesn't affect the retry; the credential
# identifies the paid order. This verifies payment and returns the claim page URL.
RETRY=$(curl -s -X POST https://api.lightningenable.com/api/shopify/my-store/checkout \
-H "Content-Type: application/json" \
-H "Authorization: L402 ${MACAROON}:${PREIMAGE}" \
-d "$CHECKOUT_BODY")

# claimPageUrl carries the claim token as ?token=... (when the merchant has a claim page).
# Hand the URL to the human buyer, or extract the token to claim via the API:
CLAIM_PAGE_URL=$(echo "$RETRY" | jq -r '.claimPageUrl')
CLAIM_TOKEN="${CLAIM_PAGE_URL##*token=}"

# 5. Claim the order (the order is already marked paid, so the token alone suffices —
# including the Authorization header again is harmless)
curl -X POST https://api.lightningenable.com/api/shopify/my-store/claim \
-H "Content-Type: application/json" \
-d "{
\"claimToken\": \"${CLAIM_TOKEN}\",
\"email\": \"customer@example.com\",
\"shippingAddress\": {
\"firstName\": \"Jane\",
\"lastName\": \"Doe\",
\"address1\": \"123 Main St\",
\"city\": \"Austin\",
\"province\": \"TX\",
\"zip\": \"78701\",
\"country\": \"US\"
}
}"

# 6. Track the order — the claim token keeps working as a read credential after the claim
curl "https://api.lightningenable.com/api/shopify/my-store/orders/${ORDER_ID}?claimToken=${CLAIM_TOKEN}"

Edge Cases

ScenarioWhat Happens
BTC price moves after checkoutThe sats amount is locked in the Lightning invoice at checkout time. The invoice expires in ~10 minutes.
Product goes out of stockChecked at checkout time against cached catalog. At claim time, Shopify's decrement_obeying_policy handles inventory.
Claim token used twiceSecond claim is rejected (400, already claimed) — tokens are single-use for claiming. The token remains valid for order-status lookups.
Wallet returns no preimageThe L402 checkout retry isn't possible (e.g., some Strike-to-Strike payments). If email was provided at checkout, the buyer receives the claim URL automatically once the payment lands — otherwise contact the merchant.
Shopify order creation failsOrder stays in PaidAwaitingDetails. Payment is safe. Can be retried via admin.
BTC price feed unavailableCheckout returns 503 with a correlationId rather than quoting a stale rate. Retry shortly.
No tax location providedCheckout returns 400 if the required X-Buyer-Location header is missing or blank. There is no fallback.
Invoice expires before paymentOrder stays in PendingPayment. Agent must create a new checkout.
Claim window expires before claimingContact the merchant for manual resolution. Payment is recorded. Default window is 30 days (configurable 1–365).