# Lightning Enable Documentation — llms-full.txt > Lightning Enable — infrastructure for agent commerce over Lightning. > A commerce orchestration layer connecting platforms and AI agents to > Bitcoin Lightning payment providers (Strike, OpenNode). Lightning Enable > does not hold funds; the merchant's payment provider facilitates custody > and settlement. This file is the full documentation corpus of https://docs.lightningenable.com, generated from the Markdown sources by docs-site/scripts/generate-llms-full.mjs (do not edit by hand — regenerate with `npm run generate-llms-full` from docs-site/). The legal section (terms of service, privacy policy, data processing agreement) is excluded to keep this file compact; find it at https://docs.lightningenable.com/legal/terms-of-service. For a short curated index instead, see https://docs.lightningenable.com/llms.txt. ============================================================================== # Authentication Source: https://docs.lightningenable.com/api-reference/authentication ============================================================================== # Authentication Authenticated endpoints take your merchant API key in the `X-API-Key` header. One key covers payments, the L402 producer API, and the merchant self-service endpoints. ```bash curl https://api.lightningenable.com/api/merchant/me \ -H "X-API-Key: YOUR_API_KEY" ``` A few endpoints are public and need no key: `/health`, `GET /api/payments/{invoiceId}/status`, the hosted checkout page at `/pay/{token}`, L402-gated proxy routes (`/l402/proxy/*`, which take an L402 credential instead), and the signup endpoints. ## API key format **Treat your API key as an opaque string.** Store it exactly as issued and send it back verbatim. Some keys start with `lgw_` and some don't; both are valid, and the format may change, so don't validate a prefix or a length. ## Getting your API key You get a key when you create an account: - **Free Producer Sandbox**: sign up with your email at `/dashboard/signup`; open the confirmation link in your email, type the email address you signed up with, and press **Confirm and create my account**; the key is shown after you confirm. Opening the link alone doesn't create the account. - **L402 Fast Lane**: pay a small Lightning invoice; the key is in the response. See [Activate with Lightning](/getting-started/activate-with-lightning). - **Card checkout**: the key is shown on the success page. View or regenerate it any time at **Dashboard → Settings** (`/dashboard/settings`). ## Keeping your key safe - Keep it out of version control. Load it from an environment variable or a secret manager. - Never put it in browser code. For client-side payment polling, use the public `GET /api/payments/{invoiceId}/status`. - Rotate it if it may have leaked, or when someone with access leaves. ## Key rotation Regenerate the key at **Dashboard → Settings → API Key → Regenerate**, or with `POST /api/merchant/regenerate-key`. **The old key stops working immediately**, so have the new one ready to deploy before you rotate. ## Error responses ### Missing API key ```http HTTP/1.1 401 Unauthorized { "error": "API key required", "message": "Please provide API key in X-API-Key header" } ``` ### Invalid API key ```http HTTP/1.1 401 Unauthorized { "error": "Invalid API key", "message": "The provided API key is invalid or inactive" } ``` You also get this if the account was deactivated. A request that authenticates but hits a subscription or feature gate gets a `403` instead; see the [Error Reference](/api-reference/errors#subscription-errors). ## Failed-authentication throttling Failed authentications are counted **per IP address**. More than 20 failures (missing or invalid key) in a 60-second window blocks authenticated requests from that IP until the window ends: ```http HTTP/1.1 429 Too Many Requests Retry-After: 42 { "error": "Too many failed authentication attempts", "message": "Slow down and try again in 42 seconds" } ``` This means your integration keeps sending a wrong or stale key. **Fix the key; don't retry with it**, because each retry records another failure. The general rate limiter's `429` says `"Too many requests"` instead; see [Rate Limiting](/api-reference/rate-limiting#failed-authentication-throttling). ## Test your key ```bash curl https://api.lightningenable.com/api/merchant/me \ -H "X-API-Key: YOUR_API_KEY" ``` `200` means the key works; `401` means it's missing or invalid. `/health` needs no key, so use it to tell an outage from a key problem. ## Code examples ### JavaScript ```javascript const response = await fetch('https://api.lightningenable.com/api/merchant/me', { headers: { 'X-API-Key': process.env.LIGHTNING_ENABLE_API_KEY } }); if (response.status === 401) throw new Error('Invalid API key'); const account = await response.json(); ``` ### C# ```csharp var client = new HttpClient { BaseAddress = new Uri("https://api.lightningenable.com") }; client.DefaultRequestHeaders.Add("X-API-Key", config["LightningEnable:ApiKey"]); var account = await client.GetFromJsonAsync("/api/merchant/me"); ``` ### Python ```python import os import requests response = requests.get( 'https://api.lightningenable.com/api/merchant/me', headers={'X-API-Key': os.environ['LIGHTNING_ENABLE_API_KEY']}, ) response.raise_for_status() ``` ## Next steps - [Request Headers](/api-reference/headers) - Idempotency, correlation IDs, and API versioning - [Payments API](/api-reference/payments) - Create payments - [Webhooks](/api-reference/webhooks) - Payment notifications - [Errors](/api-reference/errors) - Error handling ============================================================================== # Error Reference Source: https://docs.lightningenable.com/api-reference/errors ============================================================================== # Error Reference This page lists the errors the Lightning Enable API returns, by area. ## Error Response Format Every error body has an `error` field. Dispatch on the **HTTP status** first, then on `error`: - **Coded errors** carry a stable snake_case `error` (for example `invalid_expiry`, `payment_provider_not_configured`) and a human-readable `message`. Match on the code. - **Older errors** carry a prose `error` (for example `"Invalid API key"`), sometimes with a `message`. Match these on the HTTP status; the wording can change. - **The L402 producer endpoints** (`/api/l402/challenges*`) return `application/problem+json` with a stable `type` URI (`https://lightningenable.com/problems/`) plus the same `error` and `message` members. ```json { "error": "Invalid API key", "message": "The provided API key is invalid or inactive" } ``` `403` responses from subscription and feature enforcement add snake_case context fields: | Field | Type | Present on | Description | |-------|------|-----------|-------------| | `error` | string | all errors | Short error identifier (e.g., `"Invalid API key"`, `"Subscription required"`) | | `message` | string | most errors | Human-readable description | | `action_required` | string | 403 subscription/feature errors | What to do next: `contact_support`, `subscribe`, `update_payment_method`, `renew_subscription`, `upgrade_plan` | | `current_plan` | string \| null | some 403 errors | Your current plan tier — one of `free`, `individual`, `l402`. An account carrying a tier value we do not recognize is one exception: that value is echoed back raw so an operator can see what needs fixing. It is `null` when the account has no plan tier on file (a blank tier — for example an admin-created account that has not been assigned one). Handle an unexpected value rather than assuming the enum | | `required_plan` | string \| null | 403 feature errors | Plan tier that would grant the feature, or `null` when no plan grants it | | `subscription_status` | string | some 403 errors | Current Stripe subscription status | | `feature` | string | 403 feature errors | The gated feature identifier | | `support_email` | string \| null | 403 account-inactive and feature-gate errors only | The address to email when `action_required` is `contact_support`. It is `null` on feature-gate errors you can resolve yourself, such as an upgrade, and absent from the subscription-state 403s (`subscribe`, `update_payment_method`, `renew_subscription`) | Every error response also includes an `X-Correlation-Id` response header that you can use when contacting support. See [Request Headers](/api-reference/headers) for details. ### Validation Errors (Model Binding) When a request body fails model binding or data-annotation validation (missing required fields, wrong types, out-of-range values), ASP.NET Core returns the standard RFC 9457 validation problem shape — **not** the `{error}` shape above: ```json { "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1", "title": "One or more validation errors occurred.", "status": 400, "errors": { "Amount": [ "Amount must be 0 or greater (0 = demo mode)" ], "OrderId": [ "OrderId is required" ] }, "traceId": "00-a1b2c3d4e5f6...-01" } ``` Check the `Content-Type`: validation problems are returned as `application/problem+json`, with per-field messages in the `errors` dictionary. Business-logic failures (e.g., duplicate order ID) use the `{error}` JSON shape instead. `POST /api/l402/challenges` is the exception: its binding failures use the Lightning Enable problem shape, with `error` / `message` members and a stable `type`. See [Invalid Challenge Request](#invalid-challenge-request) and [Unsupported Media Type](#unsupported-media-type). ### Unhandled Exceptions Unhandled server errors return a correlation ID for support inquiries: ```json { "error": "An error occurred processing your request.", "correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "timestamp": "2026-01-09T12:00:00Z", "support": "support@lightningenable.com" } ``` `correlationId` matches the `X-Correlation-Id` response header; include it when you contact support. The `support` field appears on `5xx` responses only. Production responses never include exception details. ## HTTP Status Codes ### Success Codes | Code | Meaning | When Used | |------|---------|-----------| | `200 OK` | Request succeeded | GET, PUT, POST (when returning data) | | `201 Created` | Resource created | POST when creating payments, refunds, proxies | | `204 No Content` | Success, no response body | DELETE operations | ### Client Error Codes | Code | Meaning | Common Causes | |------|---------|---------------| | `400 Bad Request` | Invalid request | Missing required fields, invalid format, validation errors, duplicate order ID | | `401 Unauthorized` | Authentication failed | Missing/invalid API key, invalid webhook signature | | `402 Payment Required` | L402 payment needed | Accessing L402-protected endpoints without valid token | | `403 Forbidden` | Access denied | Inactive account, subscription issues, feature not available | | `404 Not Found` | Resource not found | Invalid invoice ID, order ID, proxy ID | | `409 Conflict` | Conflicts with the current state | Idempotency key reused for a different charge, a renewal refusal, a store state-machine refusal. Duplicate order IDs return `400`, not `409` | | `414 URI Too Long` | L402 resource path too long | A proxied path over 848 characters (`resource_too_long`) | | `415 Unsupported Media Type` | Request body isn't JSON | No body, or a `Content-Type` other than `application/json` | | `429 Too Many Requests` | Rate limit or auth-failure throttle | Too many requests in the window, or too many failed authentication attempts from your IP | | `499 Client Closed Request` | You disconnected before the response was ready | Recorded in logs, never read by you: there's no body. Seen when your client timeout is shorter than the time an invoice takes to mint | ### Server Error Codes | Code | Meaning | When Used | |------|---------|-----------| | `500 Internal Server Error` | Server error | Unexpected errors, includes correlationId | | `502 Bad Gateway` | Upstream error | The payment provider refused the request, or a proxied API couldn't be reached | | `503 Service Unavailable` | Temporary outage | Payment provider down or busy, or a seller's payment provider not set up | | `504 Gateway Timeout` | Upstream timeout | A proxied API didn't answer in time | --- ## Authentication Errors ### Missing API Key **HTTP Status:** `401 Unauthorized` ```json { "error": "API key required", "message": "Please provide API key in X-API-Key header" } ``` **Cause:** Request missing the `X-API-Key` header. **Solution:** Include your API key in every request: ```bash curl -H "X-API-Key: YOUR_API_KEY" https://api.lightningenable.com/api/payments ``` ### Invalid API Key **HTTP Status:** `401 Unauthorized` ```json { "error": "Invalid API key", "message": "The provided API key is invalid or inactive" } ``` **Cause:** API key doesn't match any active merchant account. **Solution:** - Verify you're using the correct API key (view it at **Dashboard → Settings**) - Check if your account is active - Contact support if you've lost your API key ### Too Many Failed Authentication Attempts **HTTP Status:** `429 Too Many Requests` **Header:** `Retry-After: ` ```json { "error": "Too many failed authentication attempts", "message": "Slow down and try again in 42 seconds" } ``` **Cause:** More than 20 failed authentication attempts from your IP within a 60-second window. This is a per-IP throttle, separate from the general rate limiter. **Solution:** Fix your API key — do **not** retry with the same key. Retrying failed authentications only keeps the throttle engaged. See [Failed-Authentication Throttling](/api-reference/rate-limiting#failed-authentication-throttling). ### Server Configuration Error **HTTP Status:** `500 Internal Server Error` ```json { "error": "Server configuration error" } ``` **Cause:** Server configuration issue on the Lightning Enable side. **Solution:** Contact support@lightningenable.com with your correlation ID. --- ## Subscription Errors ### Account Inactive **HTTP Status:** `403 Forbidden` ```json { "error": "Account inactive", "message": "Your account is inactive. Please contact support.", "action_required": "contact_support", "support_email": "support@lightningenable.com" } ``` **Cause:** Merchant account has been deactivated. **Solution:** Contact support@lightningenable.com to reactivate. ### Subscription Required **HTTP Status:** `403 Forbidden` ```json { "error": "Subscription required", "message": "Your plan tier requires an active subscription. Please subscribe to continue using the service.", "current_plan": "individual", "action_required": "subscribe" } ``` **Cause:** Paid-tier account without a valid Stripe subscription. **Solution:** Subscribe at [lightningenable.com](https://lightningenable.com). ### Subscription Not Active **HTTP Status:** `403 Forbidden` ```json { "error": "Subscription not active", "message": "Your subscription payment is past due. Please update your payment method to continue using the service.", "subscription_status": "past_due", "action_required": "update_payment_method" } ``` **Possible Status Values:** | Status | Message summary | `action_required` | |--------|-----------------|-------------------| | `past_due` | Payment is past due | `update_payment_method` | | `canceled` | Subscription canceled | `renew_subscription` | | `unpaid` | Subscription unpaid | `renew_subscription` | | `incomplete` | Setup incomplete | `renew_subscription` | | `incomplete_expired` | Setup expired | `renew_subscription` | :::note `action_required` is `update_payment_method` only for `past_due`; every other inactive status returns `renew_subscription`. The `message` text varies per status. ::: ### Subscription Period Expired **HTTP Status:** `403 Forbidden` ```json { "error": "Subscription period expired", "message": "Your subscription billing period has expired. Please renew your subscription to continue using the service.", "subscription_status": "active", "current_period_end": "2026-01-15T00:00:00.0000000Z", "action_required": "renew_subscription" } ``` **Cause:** The subscription's `CurrentPeriodEnd` has passed even though the status may still show `active`. This typically occurs when a Stripe webhook is delayed. The middleware catches expired billing periods as a safety net. **Solution:** Renew your subscription or wait for Stripe to process the renewal. If your payment method is valid, this should resolve automatically once the Stripe webhook updates the billing period. #### Lightning-paid period expired An account paid in sats over L402 gets its own variant of this `403` once its paid-through date passes. It carries a `renew` object naming the endpoint that takes the payment: ```json { "error": "Subscription period expired", "message": "Your Lightning-paid subscription period has ended. Renew now with an L402 payment, or the account moves to the Free Producer Sandbox within 24 hours.", "subscription_status": "active", "current_period_end": "2026-11-10T14:03:22.0000000Z", "action_required": "renew_subscription", "renew": { "endpoint": "/api/merchant/subscription/renew" } } ``` **Cause:** The account's `paymentMethod` is `l402`, it has no Stripe subscription, and `currentPeriodEnd` has passed. Nothing renews a sats period automatically. **Solution:** Pay at `renew.endpoint` (see [Renew in sats](/getting-started/activate-with-lightning#renew-in-sats)). That endpoint is exempt from subscription enforcement, so the lapsed account can reach it. If you don't pay, a daily job moves the account to the Free Producer Sandbox, after which the API key keeps working within the Free limits. Agents should branch on `action_required` and, when `renew` is present, pay there rather than open card checkout. ### Feature Not Available **HTTP Status:** `403 Forbidden` ```json { "error": "Feature not available", "message": "L402 Agentic Commerce is not enabled for your plan. Upgrade to an Agentic Commerce plan to use the L402 producer API.", "feature": "l402", "current_plan": "individual", "required_plan": "individual", "action_required": "upgrade_plan", "support_email": null } ``` **Gated features:** | `feature` | Endpoints | `required_plan` | `action_required` | |-----------|-----------|-----------------|-------------------| | `refunds` | `/api/refunds` | `null` | `contact_support` | | `l402` | `/api/l402/challenges` | `individual` | `upgrade_plan` | Refunds send `required_plan: null` because no plan grants them: support turns them on per account, so upgrading wouldn't help. That body carries `support_email: "support@lightningenable.com"`; the `upgrade_plan` row sends `support_email: null`. :::note `current_plan` can equal `required_plan` The gate reads a per-account flag, not the plan table. All three tiers include L402, so this means the flag was never applied to your account (most often one created under a retired tier id). Contact support rather than buying the plan again. ::: See [Subscription & Plan Enforcement](/products/subscription-management) for full details on plan tiers and feature gating. ### Plan Required (store API) **HTTP Status:** `403 Forbidden` Returned when an account on the Free Producer Sandbox calls the store API (`/api/store-orders`, `/api/store-designs`, `/api/store-quotes`, `/api/store-settings`, `/api/store-listings`, `/api/community-submissions`) with its API key. [Running your own Lightning store](/products/run-your-own-store) needs a paid plan. ```json { "error": "plan_required", "message": "Running a Lightning store on the store API needs a paid Lightning Enable plan (Agentic Commerce). The Free Producer Sandbox does not include it. Subscribe, then retry with the same API key.", "feature": "store", "current_plan": "free", "required_plan": "individual", "action_required": "subscribe" } ``` Subscribe to Agentic Commerce, then retry with the same API key. Another account's store record is never a `403`: it answers `404 Not Found`, as if it didn't exist. ### Other store API errors | Status | `error` | When | What to do | |--------|---------|------|------------| | `401` | `unauthorized` | No valid `X-API-Key` (or legacy store key). | Send your merchant API key in `X-API-Key`. | | `400` | `invalid_submission_id` | A community-submission file route got an id with characters other than `A-Z a-z 0-9 _ -`. | Use a submission id made of those characters. | | `409` | `payment_hash_in_use` | The Lightning payment hash is already bound, or was bound before a re-bind, to another design or order (payment hashes are unique across all accounts). | Create a new invoice and bind its hash. | | `400` | `invoice_outlives_quote` | A custom-product order's BOLT11 stays payable later than `maxInvoiceExpiresAt` (the quote's `quoteExpiresAt` plus the allowed lifetime, one hour by default). The response also carries `maxExpiresInSeconds`, the time left until `maxInvoiceExpiresAt`. | Create the invoice with `POST /api/payments` and `expiresInSeconds` no larger than `maxExpiresInSeconds`. The store's custom checkout sends the quote's remaining validity. | | `409` | `store_tenant_violation` | A write would have touched a store record of another account. Nothing was saved. | Read the record with your own credentials and retry; contact support if it persists. | | `400` | `store_merchant_required` | A store record needs an owning account and the request had none. Nothing was saved. | Authenticate with your merchant API key in `X-API-Key`. | | `404` | `store_settings_not_set` | `GET /api/store-settings` found no stored settings for your account. The defaults apply. | Set them with `PUT /api/store-settings` if you want different values. | | `400` | `invalid_store_settings` | A store setting is out of range. The response's `field` names it. Nothing was saved. | Fix that field and send the whole settings body again. | | `409` | `store_settings_conflict` | The `rowVersion` you sent isn't the stored one: the settings changed since you read them. The response carries `currentRowVersion`. | Read the settings again, re-apply your change, and send the new `rowVersion`. | | `409` | `publishing_disabled` | A design can't be published (or approved for publishing) because `publishingEnabled` is off in your store settings. | Turn publishing on with `PUT /api/store-settings`. | | `409` | `publish_limit_reached` | The designer already holds `limit` open listings (pending review or published). | Withdraw one of the designer's listings first, or raise `maxPublishedPerDesigner`. | | `400` | `invalid_publication_action` | The publication `action` isn't `request`, `withdraw`, `approve` or `reject`. | Send a supported action. `approve` and `reject` also need `"actor": "operator"` and a `reason`. | | `400` | `invalid_listing_title` | `listingTitle` is empty, longer than 120 characters, or contains control characters. | Send a shorter title, or leave it out. | | `404` | `listing_not_found` | No listing in your account has this `listingId`. | Check the id; another account's listing is never visible. | | `400` | `invalid_listing_id` | `listingId` isn't a listing id, or a quote request sent both `designId` and `listingId`. | Send one listing id as returned by the API, or `designId` alone. | | `409` | `listing_not_available` | The listing can't be quoted or ordered. The response carries `publicationStatus` and a `reason`: `not_published`, `expired`, `design_not_approved`, or `publishing_disabled` (publishing is turned off in your store settings). | Show the buyer another listing, or turn publishing back on. | | `503` | `publication_busy` | Another publication request for the same designer is in progress. The response has a `Retry-After` header. Nothing was saved. | Retry after the `Retry-After` delay. | | `409` | `listing_taken_down` | The listing this order was bought through was taken down, so the order won't be manufactured. A late payment on a cancelled order is still recorded. | Resolve the order from your store's review page, for example with a refund. | | `429` | `listing_quote_limit` | The listing already has `limit` quotes being priced, counting only quotes opened in the last 15 minutes by default. | Retry shortly. | | `400` | `invalid_reason` | A design takedown (`POST /api/store-designs/{designId}/moderation-reject`) sent a `reason` longer than `maxLength` (200) characters. Nothing was changed. | Send a shorter reason. A blank one is `400 reason_required`; a missing or non-`operator` `actor` is `400 invalid_actor`. | | `400` | `listing_quote_mismatch` | The order's `listingId` isn't the listing its quote was made for. | Quote the listing with its `listingId`, then order that quote. | --- ## Payment Errors ### Payment Not Found **HTTP Status:** `404 Not Found` ```json { "error": "Invoice 12345 not found for merchant 42" } ``` **Cause:** Invoice ID doesn't exist or doesn't belong to your merchant account. Looking up a payment by order ID returns the same shape with an order-specific message (`"Invoice not found for OrderId ORDER-123"`). **Solution:** Verify the invoice ID is correct and belongs to your account. ### Payment Provider Not Configured ```json { "error": "payment_provider_not_configured", "message": "No payment provider API key is configured on this account, so no Lightning invoice could be created and nothing was charged. ...", "docs": "https://docs.lightningenable.com/strike-setup/account-setup" } ``` **Cause:** your account has no usable credential for its payment provider (a new Free Producer Sandbox account starts this way). `message` and `docs` name the provider your account uses. **Status depends on who's asking:** | Surface | Status | Body | |---------|--------|------| | Your own calls: `POST /api/payments`, `POST /api/payments/{invoiceId}/sync`, `POST /api/l402/challenges`, `POST /api/checkout/sessions` | `400` | Tells you what to add and where (`PUT /api/merchant/strike-key`, `opennode-key`, or `nwc-connection`) | | A buyer paying you: `/l402/proxy/*`, other L402-gated routes, Shopify checkout | `503` | Says the seller hasn't finished setup, with no instructions for your account | **Solution:** add a credential under **Dashboard → Settings**, or with the endpoint named in `message`. Retrying without one never succeeds. ### Invalid Invoice Expiry **HTTP Status:** `400 Bad Request` ```json { "error": "invalid_expiry", "message": "expiresInSeconds must be a whole number between 60 and 86400 seconds (1 minute to 24 hours), or omitted to use the payment provider's default invoice lifetime. Nothing was created.", "minExpiresInSeconds": 60, "maxExpiresInSeconds": 86400 } ``` **Cause:** `POST /api/payments` got an `expiresInSeconds` outside 60 to 86400, or a value that isn't a whole number (a fraction, a string, or a number too large to read). **Solution:** Send a value in range, or leave the field out to use the provider's default lifetime. ### Payment Provider Rejected the Invoice **HTTP Status:** `502 Bad Gateway` Illustrative body (`message` and `providerStatus` carry what Strike actually answered): ```json { "error": "payment_provider_rejected", "message": "The payment provider (strike) refused to create the invoice (HTTP 422): INVALID_DATA: Invalid data. (bolt11.expiryInSeconds: Value is out of range.) Nothing was created. Fix the request and retry; if it persists, contact support@lightningenable.com.", "provider": "strike", "providerStatus": 422, "retryable": false } ``` **Cause:** Strike answered `POST /api/payments`'s invoice request with a 4xx error, so no invoice exists. `message` carries only Strike's error code, message and per-field validation messages; `providerStatus` is Strike's HTTP status. **Solution:** Fix what `message` names and retry. A `401` or `403` in `providerStatus` means Strike rejected your Strike API key. ### Payment Provider Unavailable (Invoice Creation) **HTTP Status:** `502 Bad Gateway` Illustrative body: ```json { "error": "payment_provider_unavailable", "message": "The payment provider (strike) is unavailable (HTTP 503), so no invoice was created. Retry later; if it persists, contact support@lightningenable.com.", "provider": "strike", "providerStatus": 503, "retryable": true } ``` **Cause:** Strike answered `POST /api/payments`'s invoice request with a 5xx error, so no invoice exists. **Solution:** Retry the same request later. ### NWC Request Refused (currency or amount) **HTTP Status:** `400 Bad Request` Your account settles into your own Lightning wallet over [Nostr Wallet Connect](/nwc-setup/account-setup). A Lightning wallet prices in satoshis and Lightning Enable converts nothing, so `POST /api/payments` and `POST /api/checkout/sessions` refuse three requests before anything reaches the wallet. Nothing is created. Every body has the shape `{ error, message, provider: "nwc", …, docs }`: | `error` | Cause | Extra field | |---------|-------|-------------| | `currency_not_supported` | A fiat currency. Both routes default `currency` to `USD`, so omitting it returns this too | `supportedCurrencies: ["BTC", "SAT"]` | | `amount_too_small` | The amount rounds to less than 1 satoshi | `minimumSats: 1` | | `amount_too_large` | The amount is above 21,000,000 BTC (`/api/payments` only; checkout caps `amount` at 1,000,000) | `maxSats: 2100000000000000` | ```json { "error": "currency_not_supported", "message": "Your payment lane is a Nostr Wallet Connect wallet, which cannot price a USD invoice: it settles in Bitcoin and Lightning Enable performs no currency conversion. Send currency \"BTC\" (amount in bitcoin) or \"SAT\" (amount in satoshis), or configure a Strike API key, which supports fiat-denominated invoices. Nothing was created. Details: https://docs.lightningenable.com/nwc-setup/account-setup", "provider": "nwc", "supportedCurrencies": ["BTC", "SAT"], "docs": "https://docs.lightningenable.com/nwc-setup/account-setup" } ``` **Solution:** Send `BTC` (amount in bitcoin) or `SAT` (whole satoshis), between 1 satoshi and 21,000,000 BTC. To invoice in fiat, configure a Strike API key and switch your payment provider to Strike. ### Duplicate Order **HTTP Status:** `400 Bad Request` ```json { "error": "Invoice already exists for OrderId ORDER-123" } ``` **Cause:** A payment with this order ID already exists for your merchant account. **Solution:** Use a unique order ID per payment. To retry a creation safely, send an `X-Idempotency-Key` header; see [Request Headers](/api-reference/headers). To detect this case, match the `400` status plus the `"Invoice already exists"` prefix. Field-level failures (amount out of range, missing order ID, malformed URL) return the [validation problem shape](#validation-errors-model-binding) instead. --- ## Refund Errors ### Invalid Refund Request **HTTP Status:** `400 Bad Request` ```json { "error": "Cannot refund invoice in status 'pending'. Only paid, underpaid, processing, or partially-refunded payments can be refunded." } ``` **Refundable Statuses:** Only invoices with status `paid`, `underpaid`, `processing`, or `refunded` (for partial refunds) can be refunded. Strike refunds and Lightning refunds also need `amountSatoshis`. An account that settles through its own wallet over Nostr Wallet Connect can't refund through the API (`400`): refund the payer from your wallet app. ### Refund Not Found **HTTP Status:** `404 Not Found` ```json { "error": "Refund 67890 not found for merchant 42" } ``` ### Invoice Not Found (for refund) **HTTP Status:** `400 Bad Request` (on refund creation) or `404 Not Found` (when listing refunds by invoice) ```json { "error": "Invoice 12345 not found for merchant 42" } ``` --- ## L402 Protocol Errors ### Payment Required (402) **HTTP Status:** `402 Payment Required` **Headers:** ```http WWW-Authenticate: L402 macaroon="AgEB...", invoice="lnbc..." X-L402-Error: No Authorization header provided ``` **Body:** ```json { "error": "Payment Required", "message": "Pay the Lightning invoice to access this API", "proxy": { "id": "my-api-1234", "name": "My API", "description": "AI services monetized with Lightning" }, "l402": { "macaroon": "AgEBYXBpLmxpZ2h0bmluZ2VuYWJsZS5jb20...", "invoice": "lnbc100n1pj...", "amount_sats": 100, "payment_hash": "abc123...", "expires_at": "2026-01-09T13:00:00Z" }, "instructions": { "step1": "Pay the Lightning invoice using any Lightning wallet", "step2": "Copy the preimage (proof of payment) from your wallet", "step3": "Include in request: Authorization: L402 : (or Authorization: Payment method=\"lightning\", preimage=\"\")" } } ``` ### Invalid L402 Credential **HTTP Status:** `402 Payment Required` A refused credential gets a fresh challenge, not a `401`. The reason is in the `X-L402-Error` header and in `message`. | `X-L402-Error` (prefix) | Cause | |---------------|-------| | `No Authorization header provided` | No credential sent | | `Invalid authorization scheme` | A scheme other than `L402`, `LSAT`, or `Payment` | | `Invalid payment credential format` | Not `L402 :` or `Payment method="lightning", preimage="..."` | | `Invalid preimage format` | The preimage isn't 64 hex characters | | `Preimage does not match payment hash` | The preimage is for a different invoice | | `Token not valid for this merchant` | The token was issued by another seller | | `Token amount does not match required price` | The endpoint's price changed since the token was bought | | Anything else | The macaroon was altered, expired, or issued for another path | ### Payment Provider Busy **HTTP Status:** `503 Service Unavailable` **Headers:** `Retry-After: 2` ```json { "error": "payment_provider_busy", "message": "The Lightning wallet settling this payment is busy serving other requests and did not free a slot in time, so no invoice was created and nothing was charged. This is transient — retry in a couple of seconds. Details: https://docs.lightningenable.com/nwc-setup/account-setup", "docs": "https://docs.lightningenable.com/nwc-setup/account-setup" } ``` **Cause:** the merchant settles through their own Lightning wallet over Nostr Wallet Connect, and that wallet already has as many in-flight NIP-47 requests as Lightning Enable sends it at once. A wallet app is not an API gateway, so the concurrent `make_invoice` calls it receives are bounded rather than letting a burst overwhelm it. **Nothing was invoiced and nothing was charged** — the request is refused before it reaches the wallet. Honor `Retry-After` and try again; a burst of immediate retries lands on the same saturated wallet. Merchants on the Strike or OpenNode lane never see this error. If it persists for more than a few seconds, the wallet itself is likely unreachable — check that the wallet app is online and the connection string is still valid. On `POST /api/l402/challenges` the same failure arrives as an RFC 9457 problem document (`application/problem+json`, `"type": "https://lightningenable.com/problems/payment_provider_busy"`) carrying the same `error`, `message` and `docs` members. ### Invalid Challenge Request **HTTP Status:** `400 Bad Request` (`application/problem+json`) ```json { "type": "https://lightningenable.com/problems/invalid_request", "title": "Invalid request", "status": 400, "detail": "priceSats must be a whole number of sats ≥ 1 (your plan's maximum is 1,000 sats).", "error": "invalid_request", "message": "priceSats must be a whole number of sats ≥ 1 (your plan's maximum is 1,000 sats).", "errors": { "priceSats": ["priceSats must be a whole number of sats ≥ 1 (your plan's maximum is 1,000 sats)."] }, "example": "{\"resource\":\"/hello\",\"priceSats\":10,\"description\":\"Hello\"}" } ``` **Cause:** The body of `POST /api/l402/challenges` failed validation: `resource` is missing or longer than 848 characters, `priceSats` is missing, below 1, fractional, or not a number, or the body isn't valid JSON. When your plan caps the price, the `priceSats` message quotes that maximum; on a plan with no price cap it names no maximum. **Solution:** Fix the fields named in `errors` and resend. `example` shows a valid body. Nothing was minted or invoiced. ### Unsupported Media Type **HTTP Status:** `415 Unsupported Media Type` (`application/problem+json`) ```json { "type": "https://lightningenable.com/problems/unsupported_media_type", "title": "Unsupported media type", "status": 415, "detail": "Send Content-Type: application/json and a JSON body like {\"resource\":\"/hello\",\"priceSats\":10,\"description\":\"Hello\"}", "error": "unsupported_media_type", "message": "Send Content-Type: application/json and a JSON body like {\"resource\":\"/hello\",\"priceSats\":10,\"description\":\"Hello\"}" } ``` **Cause:** `POST /api/l402/challenges` was called with no body, or with a `Content-Type` other than `application/json`. **Solution:** Send the header `Content-Type: application/json` and a JSON body. ### Plan Price Limit **HTTP Status:** `402 Payment Required` (`application/problem+json`) ```json { "type": "https://lightningenable.com/problems/plan_price_limit", "title": "Plan price limit reached", "status": 402, "detail": "Plan 'free' caps challenge price at 1000 sats. Requested: 5000. Upgrade to remove the cap.", "error": "plan_price_limit", "message": "Plan 'free' caps challenge price at 1000 sats. Requested: 5000. Upgrade to remove the cap.", "current_plan": "free", "max_price_sats": 1000, "upgrade_url": "https://api.lightningenable.com/checkout?plan=individual" } ``` **Cause:** `priceSats` on `POST /api/l402/challenges` is above your plan's per-challenge cap. Like the monthly volume cap (`plan_volume_limit`) and the endpoint cap (`plan_endpoint_limit`), it answers `402`. **Solution:** Lower `priceSats` to `max_price_sats` or less, or upgrade. A merchant with no payment provider key gets `400 payment_provider_not_configured` before any plan cap is checked. No plan can mint without a provider key, so upgrading wouldn't help. ### Reserved Resource **HTTP Status:** `400 Bad Request` (`application/problem+json`) ```json { "type": "https://lightningenable.com/problems/reserved_resource", "title": "Reserved resource", "status": 400, "detail": "Resources beginning with 'renew:' or 'signup:' are reserved for Lightning Enable's own signup and subscription flows.", "error": "reserved_resource", "message": "Resources beginning with 'renew:' or 'signup:' are reserved for Lightning Enable's own signup and subscription flows." } ``` **Cause:** `POST /api/l402/challenges` was called with a `resource` that starts with `renew:` or `signup:` (case-insensitive, after surrounding whitespace is trimmed). Those prefixes belong to the Lightning renewal and the L402 Fast Lane signup. **Solution:** Pick a resource name with a different prefix. Nothing was minted or invoiced. ### Proxy Not Found **HTTP Status:** `404 Not Found` ```json { "error": "Proxy not found", "message": "No active proxy configuration found for ID: invalid-proxy" } ``` ### Proxy Unavailable **HTTP Status:** `404 Not Found` ```json { "error": "Proxy unavailable", "message": "This API proxy is currently unavailable" } ``` **Cause:** The merchant account owning the proxy is inactive. ### L402 Proxy Gateway Errors **Bad Gateway (502):** ```json { "error": "Bad Gateway", "message": "Unable to connect to the target API", "proxy_id": "my-api-1234", "details": "Connection refused" } ``` **Gateway Timeout (504):** ```json { "error": "Gateway Timeout", "message": "The target API did not respond in time", "proxy_id": "my-api-1234" } ``` --- ## Webhook Errors The provider webhook receivers (`/api/webhooks/strike`, `/api/webhooks/opennode`) answer your payment provider, not you. For problems with webhooks sent to **your** callback URL, see [Webhooks → Troubleshooting](/api-reference/webhooks#troubleshooting). --- ## Rate Limiting Errors ### Rate Limit Exceeded **HTTP Status:** `429 Too Many Requests` **Header:** `Retry-After: ` ```json { "error": "Too many requests", "message": "Rate limit exceeded. Please try again later.", "retryAfter": 60 } ``` **Solution:** wait `Retry-After` seconds (the body's `retryAfter` is the same value), then retry. See [Rate Limiting](/api-reference/rate-limiting) for the limits per route. ### Auth-Failure Throttle (Distinct 429) A separate 429 — see [Too Many Failed Authentication Attempts](#too-many-failed-authentication-attempts) under Authentication Errors above. Distinguish the two by the `error` string: `"Too many requests"` (rate limiter, wait and retry) vs. `"Too many failed authentication attempts"` (auth throttle, fix your key). --- ## Payment Provider Errors on L402 Routes When a seller's payment provider can't issue a challenge invoice, the proxy and other L402-gated routes answer: | Status | `error` | Cause | |--------|---------|-------| | `502` | `Payment provider rejected your credentials` | The provider answered `401` or `403`: the seller's API key is invalid, revoked, or lacks invoice permission | | `502` | `Payment provider error` | The provider answered another `4xx` | | `503` | `Payment provider unavailable` | The provider answered `5xx` | | `503` | `Payment provider request timed out` | The provider didn't answer in time | | `503` | `Payment provider unreachable` | The provider couldn't be reached | | `503` | `payment_provider_busy` | The seller's own NWC wallet is busy; see [Payment Provider Busy](#payment-provider-busy) | | `503` | `challenge_persist_failed` | The challenge couldn't be recorded, so no invoice was issued | | `503` | `amount_too_small` | The seller's NWC wallet can't invoice the price: it is below 1 satoshi (for example a 0-sat pricing rule). Only the seller can fix it, so retrying won't help | | `414` | `resource_too_long` | The requested path is over 848 characters | Each body carries a `message` saying what to check. Nothing was invoiced in any of these cases, so retrying a `503` is safe; it succeeds only once the cause clears (never for `amount_too_small` until the seller changes the price). On `POST /api/l402/challenges` the same failures are `502 provider_rejected` and `503 provider_unavailable` (problem documents). On `POST /api/payments` they're [`502 payment_provider_rejected`](#payment-provider-rejected-the-invoice) and [`502 payment_provider_unavailable`](#payment-provider-unavailable-invoice-creation). --- ## Stripe Integration Errors ### Checkout Session Errors **HTTP Status:** `400 Bad Request` ```json { "error": "Failed to create checkout session. Please try again." } ``` **Common Causes:** - Invalid email format - Missing required fields - Stripe API error ### Subscription Result Errors **Missing Session ID:** ```json { "error": "session_id is required" } ``` **Session Not Found:** ```json { "error": "Checkout session not found" } ``` **Payment Not Completed:** ```json { "error": "Payment not completed", "paymentStatus": "unpaid" } ``` **Customer Not Found:** ```json { "error": "Customer not found for this session" } ``` **Merchant Not Found:** ```json { "error": "Merchant account not found. Please wait a moment and try again." } ``` ### Customer Portal Errors **HTTP Status:** `401 Unauthorized` ```json { "error": "API key authentication required" } ``` **HTTP Status:** `400 Bad Request` ```json { "error": "Merchant does not have a Stripe customer ID. Please subscribe first." } ``` --- ## Proxy Management Errors ### Invalid Target URL **HTTP Status:** `400 Bad Request` ```json { "error": "Invalid target URL. Must be a valid HTTP or HTTPS URL." } ``` ### Invalid Path Pattern **HTTP Status:** `400 Bad Request` ```json { "error": "Invalid path pattern. Must start with '/' and be a valid glob pattern." } ``` ### Proxy Not Found **HTTP Status:** `404 Not Found` ```json { "error": "Proxy not found" } ``` ### Endpoint Pricing Not Found **HTTP Status:** `404 Not Found` ```json { "error": "Endpoint pricing not found" } ``` --- ## Merchant Settings Errors | Status | `error` | When | |--------|---------|------| | `400` | `Strike API key is required` / `OpenNode API key is required` | Empty key on `PUT /api/merchant/strike-key` or `opennode-key` | | `400` | `Invalid webhook URL format` | `webhookUrl` isn't an absolute URL | | `400` | `Provider must be 'strike', 'opennode' or 'nwc'` | Unknown value on `PUT /api/merchant/payment-provider` | | `400` | (names the bad part) | A malformed connection string on `PUT /api/merchant/nwc-connection` | | `404` | `Merchant not found` | The account behind the key no longer exists | --- ## Lightning renewal errors These come from `GET` and `POST /api/merchant/subscription/renew`, which renews Agentic Commerce in sats over L402. Every call needs your merchant API key; a missing or wrong key gets the [authentication errors](#authentication-errors) above. Branch on `error`. For the request flow, see [Renew with Lightning (L402)](/api-reference/merchant-settings#renew-with-lightning-l402). If a `409`, `403`, or `503` arrives after you paid, nothing was credited. Keep the token: it stays redeemable, until it expires 7 days after its challenge was issued, once the cause is fixed, and each payment is credited at most once. A `401 invalid_l402_token` is permanent for that token (see [invalid_l402_token](#invalid_l402_token)). ### stripe_subscription_active **HTTP Status:** `409 Conflict` ```json { "error": "stripe_subscription_active", "message": "This account has a card subscription that is not canceled. Cancel it in the billing portal first; a Lightning renewal cannot run alongside it.", "support_email": "support@lightningenable.com" } ``` **Cause:** The account has a Stripe subscription that isn't canceled: any status other than `canceled` or `incomplete_expired`, including `active`, `trialing`, `past_due`, `unpaid`, `incomplete`, `paused`, or no recorded status. Such a subscription can still charge the card, so a sats renewal alongside it would bill you twice. **Solution:** Cancel the card subscription in the billing portal, then renew in sats. Or keep paying by card. ### business_plan_contact_support **HTTP Status:** `409 Conflict` ```json { "error": "business_plan_contact_support", "message": "Business plans are arranged directly with Lightning Enable.", "support_email": "support@lightningenable.com" } ``` **Cause:** The account is on Agentic Commerce — Business, which isn't sold in sats. **Solution:** Email support@lightningenable.com. ### prepaid_cap_reached **HTTP Status:** `409 Conflict` ```json { "error": "prepaid_cap_reached", "message": "This account is already paid as far ahead as Lightning renewals allow.", "currentPeriodEnd": "2027-10-20T14:03:22Z" } ``` **Cause:** You requested a new challenge while the account is already paid more than 365 days ahead. `currentPeriodEnd` is the account's effective paid-through date. The cap applies only when requesting a challenge: redeeming a token you've already paid never returns this error. **Solution:** Wait until the paid-through date is within 365 days, then pay again. ### account_disabled **HTTP Status:** `403 Forbidden` ```json { "error": "account_disabled", "message": "This account is disabled. Contact support.", "action_required": "contact_support", "support_email": "support@lightningenable.com" } ``` **Cause:** An operator disabled the account. **Solution:** Email support@lightningenable.com. ### invalid_authorization **HTTP Status:** `400 Bad Request` ```json { "error": "invalid_authorization", "message": "Authorization header must be: L402 :. POST without an Authorization header to get a challenge." } ``` **Cause:** The `POST` carried an `Authorization` header that isn't an L402 credential with both a macaroon and a preimage. **Solution:** Send `Authorization: L402 :`, or omit the header to get a challenge. ### invalid_l402_token **HTTP Status:** `401 Unauthorized` ```json { "error": "invalid_l402_token", "message": "The L402 token is invalid, expired, minted for another account, or does not match this renewal. POST again without an Authorization header for a fresh challenge." } ``` **Cause:** One of these: - The macaroon or preimage is wrong, or the macaroon was altered. - The token expired. - The token was minted for another account's renewal. - The token was already credited to another account. - The token was minted at a price lower than the configured renewal price. After a price increase, a token minted at the old price is refused, including on replay. A price decrease never strands a token. **Solution:** `POST` without an `Authorization` header for a fresh challenge. This refusal is permanent for the token you sent. If you paid a challenge minted before a price increase, that payment is never credited; email support@lightningenable.com with the payment hash. ### renewal_contended **HTTP Status:** `503 Service Unavailable` **Headers:** `Retry-After: 1` ```json { "error": "renewal_contended", "message": "Your account was being updated by another request. Your payment is safe — retry this exact request with the same Authorization header; it is credited at most once." } ``` **Cause:** Another request changed the account while this payment was being credited, several times in a row. Nothing was credited. **Solution:** Retry the same request with the same `Authorization` header after `Retry-After`. Don't pay again. ### upstream_unavailable **HTTP Status:** `503 Service Unavailable` ```json { "error": "upstream_unavailable", "message": "The payment provider did not answer. Retry shortly; nothing was invoiced." } ``` **Cause:** The payment provider that issues the renewal invoice didn't answer. A `Retry-After` header is present when the receiving wallet was busy. **Solution:** Retry shortly. Nothing was invoiced. ### challenge_persist_failed **HTTP Status:** `503 Service Unavailable` ```json { "error": "challenge_persist_failed", "message": "Could not record the payment challenge; no invoice was issued to you. Retry in a moment." } ``` **Cause:** The challenge couldn't be recorded, so no invoice was issued. **Solution:** Retry in a moment. ### l402_subscription_unavailable **HTTP Status:** `503 Service Unavailable` ```json { "error": "l402_subscription_unavailable", "message": "Lightning renewals are switched off on this deployment." } ``` **Cause:** Lightning renewals are switched off. On `GET`, the same state is `eligible: false` with `reason: "Disabled"`. **Solution:** Pay by card, or retry later. ### l402_subscription_misconfigured **HTTP Status:** `503 Service Unavailable` ```json { "error": "l402_subscription_misconfigured", "message": "Lightning renewal is not available on this deployment right now.", "support_email": "support@lightningenable.com" } ``` **Cause:** The account that receives renewal payments isn't set up on this deployment. **Solution:** Retry later, or email support@lightningenable.com. --- ## Error Handling Best Practices Branch on the HTTP status, then on `error` and the context fields: ```javascript async function call(url, options) { const response = await fetch(url, options); if (response.ok) return response.json(); const body = await response.json().catch(() => ({})); const correlationId = response.headers.get('X-Correlation-Id'); switch (response.status) { case 401: throw new Error('Check your API key'); case 403: // subscribe, update_payment_method, renew_subscription, upgrade_plan, contact_support throw new Error(`Account action needed: ${body.action_required}`); case 429: if (body.error === 'Too many failed authentication attempts') { throw new Error('Fix your API key before retrying'); } // wait Retry-After seconds, then retry break; } throw new Error(`${response.status} ${body.error}: ${body.message ?? ''} (correlation ${correlationId})`); } ``` Log the `X-Correlation-Id` with every failure; support can find your request by it. ## Retry Strategy Summary | Error Type | Retry? | Strategy | |------------|--------|----------| | 400 Bad Request | No | Fix request data | | 401 Unauthorized | No | Fix credentials | | 402 Payment Required | No | Complete payment | | 403 Forbidden | No | Check subscription/features | | 404 Not Found | No | Check resource ID | | 429 Rate Limited | Yes | Wait `Retry-After` seconds | | 429 Auth Throttle | No | Fix your API key first — retrying keeps the throttle engaged | | 500 Server Error | Yes | Exponential backoff | | 502 Bad Gateway | Only if `retryable` is `true` | A provider `4xx` (`payment_provider_rejected`, `provider_rejected`) needs a fix first; a `5xx` cause can be retried with backoff | | 503 Service Unavailable | Yes | Honor `Retry-After` when present, else back off | | 504 Gateway Timeout | Yes | Back off, then retry | --- ## Next Steps - [Rate Limiting](/api-reference/rate-limiting) - Detailed rate limit information - [Authentication](/api-reference/authentication) - API key setup and security - [Webhooks](/api-reference/webhooks) - Webhook configuration and signatures - [Payments API](/api-reference/payments) - Payment creation and status ============================================================================== # Request Headers Source: https://docs.lightningenable.com/api-reference/headers ============================================================================== # Request Headers Lightning Enable uses these HTTP headers for authentication, idempotency, request tracing, and versioning. ## Header Summary | Header | Direction | Required | Description | |--------|-----------|----------|-------------| | `X-API-Key` | Request | Yes | Merchant API key | | `X-Idempotency-Key` | Request | No | Prevents duplicate operations on payment endpoints | | `Idempotency-Key` | Request | No | Standard spelling, accepted on `POST /api/l402/challenges` — see [L402 challenge idempotency](#l402-challenge-idempotency) | | `X-Correlation-Id` | Request & Response | No | Distributed tracing identifier | | `X-API-Version` | Response | -- | API version running on the server | | `X-API-Deprecation` | Response | -- | Deprecation notice (reserved for future use) | | `X-Idempotency-Replayed` | Response | -- | Indicates a replayed idempotent response was returned | | `X-Total-Count` | Response | -- | Total rows matching a paged listing, ignoring paging | | `Retry-After` | Response | -- | Seconds to wait before retrying, on `429` and some `503` responses | --- ## X-Idempotency-Key Use the `X-Idempotency-Key` header to safely retry payment and refund requests without creating duplicates. If you send the same idempotency key twice, the API returns the original cached response instead of processing the request again. ### How It Works 1. Generate a unique key (UUID recommended) on the client side. 2. Include it as `X-Idempotency-Key` in your request. 3. If the request succeeds (2xx), the response is cached for **24 hours** keyed by your merchant ID + idempotency key. 4. If you retry with the same key within 24 hours, the cached response is returned immediately with the header `X-Idempotency-Replayed: true`. ### Supported Endpoints | Endpoint | Method | Description | |----------|--------|-------------| | `/api/payments` | POST | Create payment invoice | | `/api/refunds` | POST | Create refund | | `/api/checkout/sessions` | POST | Create checkout session | ### Constraints - Maximum key length: **256 characters** - Keys are scoped per merchant (two different merchants can use the same key without conflict) - Only **successful responses** (2xx) are cached. If the request fails, you can safely retry with the same key. - Cached responses expire after **24 hours** - The cache lives in server memory, so a deploy or restart clears it. Use a unique `orderId` as well: a second `POST /api/payments` with the same `orderId` is refused with `400` whatever the header says. ### Example ```bash # First request - creates the payment curl -X POST https://api.lightningenable.com/api/payments \ -H "X-API-Key: YOUR_API_KEY" \ -H "X-Idempotency-Key: order-12345-attempt-1" \ -H "Content-Type: application/json" \ -d '{ "orderId": "ORDER-12345", "amount": 49.99, "currency": "USD" }' # Response: 201 Created { "invoiceId": "1042", ... } # Retry with same key - returns cached response curl -X POST https://api.lightningenable.com/api/payments \ -H "X-API-Key: YOUR_API_KEY" \ -H "X-Idempotency-Key: order-12345-attempt-1" \ -H "Content-Type: application/json" \ -d '{ "orderId": "ORDER-12345", "amount": 49.99, "currency": "USD" }' # Response: 201 Created { "invoiceId": "1042", ... } # Response header: X-Idempotency-Replayed: true ``` ### Error Responses | Scenario | Status | Response | |----------|--------|----------| | Key exceeds 256 characters | `400 Bad Request` | `{ "error": "Idempotency key must not exceed 256 characters." }` | ### Best Practices - **Derive the key from the operation**, for example `{orderId}-create`, so every retry of that operation reuses it. Use a new key for each distinct operation. - **Retry network errors with the same key.** You get either the cached result or a first-time run. --- ## L402 challenge idempotency `POST /api/l402/challenges` has its own idempotency, and it works differently from the response cache above — it is anchored on the minted challenge rather than on a cached HTTP response, so a retry gets back the same **invoice**, not a replayed response body that may no longer be payable. | | Payment endpoints | `POST /api/l402/challenges` | |---|---|---| | Header | `X-Idempotency-Key` | `Idempotency-Key` (or `X-Idempotency-Key`) | | Max key length | 256 characters | 200 characters | | Over-long key | `400` | `400` | | What is replayed | The cached 2xx response | The minted challenge itself — same invoice, macaroon, and payment hash | | Window | 24 hours | The life of the invoice (10 minutes by default) | | Survives a restart | No (process cache) | Yes (stored on the challenge) | | Same key, different body | Cached response returned | `409` if the resource or price differs and the invoice is still live; a fresh mint once it has expired | The key can also be sent as an `idempotencyKey` body field. Replays carry `X-Idempotency-Replayed: true`. Full rules: [Producer API Reference → Idempotency](/products/agentic-commerce/producer-api-reference#idempotency). --- ## X-Correlation-Id The `X-Correlation-Id` header enables distributed request tracing across your systems and the Lightning Enable API. ### How It Works - **If you send** an `X-Correlation-Id` header with your request, the API uses your value throughout the request lifecycle. - **If you do not send one**, the API generates a new UUID automatically. - The correlation ID is **always returned** in the response `X-Correlation-Id` header. - The same ID appears in the `correlationId` field of error responses for unhandled exceptions. - All server-side log entries for the request include this ID. ### Example ```bash # Send your own correlation ID curl -X GET https://api.lightningenable.com/api/payments/1042 \ -H "X-API-Key: YOUR_API_KEY" \ -H "X-Correlation-Id: my-trace-id-12345" # Response headers include: # X-Correlation-Id: my-trace-id-12345 ``` ```bash # Let the API generate one curl -X GET https://api.lightningenable.com/api/payments/1042 \ -H "X-API-Key: YOUR_API_KEY" # Response headers include: # X-Correlation-Id: a1b2c3d4-e5f6-7890-abcd-ef1234567890 ``` Log the correlation ID on your side and include it when you contact support. --- ## X-API-Version Every API response includes an `X-API-Version` header indicating the version of the Lightning Enable API that served the request. ```http HTTP/1.1 200 OK X-API-Version: 1.0.0 ``` ### Usage - Use this header to verify which API version you are communicating with. - The version follows [semantic versioning](https://semver.org/) (major.minor.patch). - Breaking changes will increment the major version. ### X-API-Deprecation (Reserved) The `X-API-Deprecation` response header is reserved for future use. When an endpoint or API version is deprecated, this header will contain a human-readable deprecation notice with a migration deadline. Currently, no endpoints are deprecated. --- ## Next Steps - [Authentication](/api-reference/authentication) - API key setup - [Errors](/api-reference/errors) - Error handling and correlation IDs - [Rate Limiting](/api-reference/rate-limiting) - Rate limits and `Retry-After` ============================================================================== # L402 API Source: https://docs.lightningenable.com/api-reference/l402 ============================================================================== # L402 API L402 is pay-per-request API access over Lightning: the server answers `402 Payment Required` with an invoice and a macaroon, the client pays, and it retries with proof of payment. No account or card is needed to pay. Lightning Enable speaks two schemes on the same 402: classic **L402** (macaroon + preimage) and the HTTP **`Payment`** scheme (MPP, Machine Payments Protocol). One invoice backs both, so a client uses whichever it implements. See [Payment (MPP) credentials](#payment-mpp-credentials). To sell access, either put your API behind an [L402 proxy](/products/agentic-commerce/proxy-configuration) or mint challenges yourself with the [Producer API](#l402-producer-api). ## L402 Protected Proxy ```http * /l402/proxy/{proxyId}/{path} ``` Public: the proxy needs no API key. Requests without a valid credential get a 402; requests with one are forwarded to your API. ### Step 1: Request the resource ```bash curl -i https://api.lightningenable.com/l402/proxy/my-api/data ``` ### Step 2: Receive the 402 challenge ```http HTTP/1.1 402 Payment Required WWW-Authenticate: L402 macaroon="AgEL...", invoice="lnbc100n1p..." WWW-Authenticate: Payment id="k9Q3...", realm="lightning-enable", method="lightning", intent="charge", ... Link: ; rel="l402-manifest" Cache-Control: no-store ``` ```json { "error": "Payment Required", "message": "Pay the Lightning invoice to access this API", "proxy": { "id": "my-api", "name": "My API", "description": "Weather data" }, "l402": { "macaroon": "AgEL...", "invoice": "lnbc100n1p...", "amount_sats": 10, "payment_hash": "abc123...", "expires_at": "2026-11-03T13:00:00.0000000+00:00" }, "instructions": { "step1": "Pay the Lightning invoice using any Lightning wallet", "step2": "Copy the preimage (proof of payment) from your wallet", "step3": "Include in request: Authorization: L402 : (or Authorization: Payment method=\"lightning\", preimage=\"\")" } } ``` The `Link` header appears when the proxy publishes a manifest. ### Step 3: Pay the invoice Pay with any Lightning wallet that returns the **preimage** (proof of payment). Strike, LND, Alby Hub, CoinOS and CLINK wallets return it; OpenNode and Primal don't. ### Step 4: Retry with the credential ```bash curl https://api.lightningenable.com/l402/proxy/my-api/data \ -H "Authorization: L402 AgEL...:7f8a9b2c..." ``` The proxy forwards the request to your API and returns its response. ### When a credential is refused A missing, malformed, expired, or mismatched credential never gets a `401` or `403` on the proxy. It gets a **fresh 402 challenge**, with the reason in the `X-L402-Error` header and in `message`: ```http HTTP/1.1 402 Payment Required X-L402-Error: Preimage does not match payment hash ``` See [Invalid L402 Credential](/api-reference/errors#invalid-l402-credential) for the common reasons. Other proxy errors (`404` for an unknown proxy, `502`/`504` when your API can't be reached, `503` when the seller's payment provider can't issue an invoice) are in the [Error Reference](/api-reference/errors#l402-protocol-errors). ## Credential Format ``` Authorization: L402 : ``` | Component | Format | Description | |-----------|--------|-------------| | `macaroon` | Base64 | Bearer token with caveats | | `preimage` | Hex (64 characters) | 32-byte proof of payment | `LSAT` is accepted as an alias for `L402`. ## Token Reuse A classic L402 token is reusable until its `expires` caveat (1 hour by default), but only for the path, account, and price it was issued for. Key cached credentials by the **full path**, not the host: a token for `/l402/proxy/api-a/data` is refused on `/l402/proxy/api-b/data`. ## Macaroon Caveats Every macaroon Lightning Enable issues carries these first-party caveats, and **all** must hold for the token to verify. An unrecognized caveat fails verification. | Caveat | Example | Meaning | |--------|---------|---------| | `services` | `services = lightning-enable:0` | Service identifier and tier | | `path` | `path = /l402/proxy/my-api/data` | The path the token was issued for. A trailing `/*` allows sub-paths | | `merchant_id` | `merchant_id = 42` | The selling account. A token from one seller can't be replayed against another | | `charge_id` | `charge_id = 8f6c3f5e-...` | The payment provider's charge ID for the invoice | | `amount_sats` | `amount_sats = 100` | The price paid. The endpoint's current price must match, so a cheap token can't open a pricier endpoint | | `method` | `method = GET` | On proxy tokens that carry it: the HTTP method that drew the challenge | | `expires` | `expires = 1762174800` | Unix time after which the token is refused | Verification checks that `SHA256(preimage)` equals the payment hash, then the macaroon signature, then each caveat against the request. ## Payment (MPP) credentials Alongside L402, every 402 also advertises the HTTP **`Payment`** authentication scheme, the format shared by [`draft-httpauth-payment-00`](https://datatracker.ietf.org/doc/draft-httpauth-payment/) (the core scheme) and [`draft-lightning-charge-00`](https://datatracker.ietf.org/doc/draft-lightning-charge/) (the Lightning `charge` method). This is usually called **MPP** (Machine Payments Protocol). Three credential profiles are accepted, and all three are proofs of the same invoice: | Profile | `Authorization` header | Reusable? | Best for | |---|---|---|---| | Classic L402 | `L402 :` | Yes, until the macaroon expires | Existing L402 clients, LND `lnget`, Aperture-style tooling | | Legacy `Payment` (auth-params) | `Payment method="lightning", preimage=""` | Yes, until the challenge expires | Clients that only want to send a preimage | | Modern `Payment` (bearer token) | `Payment ` | **No. Single use** | Clients built on the current drafts (mppx, `l402-requests`, the Lightning Enable MCP server v1.24+) | You never opt in per merchant. The server emits both schemes for every L402-gated resource, and a client uses whichever one it implements. ### The 402 challenge A single 402 carries one `WWW-Authenticate` header per scheme. The `Payment` header is a superset: it holds the modern parameters and the legacy `invoice` / `amount` / `currency` parameters in the same value, and both drafts require clients to ignore parameters they don't know. ```http HTTP/1.1 402 Payment Required WWW-Authenticate: L402 macaroon="AgEL...", invoice="lnbc1u1p..." WWW-Authenticate: Payment id="k9Q3...", realm="lightning-enable", method="lightning", intent="charge", request="eyJhbW91bnQiOiIxMDAiLC...", expires="2026-09-11T18:30:00Z", invoice="lnbc1u1p...", amount="100", currency="sat" Cache-Control: no-store ``` | Parameter | Meaning | |---|---| | `id` | Server-computed binding over every other parameter. Verification recomputes it from the fields you echo back, so any edit to the challenge invalidates the credential. | | `realm` | Always `lightning-enable` on the hosted API. | | `method` / `intent` | Always `lightning` / `charge`. | | `request` | base64url of a JCS-canonical JSON object: `{"amount":"100","currency":"sat","methodDetails":{"invoice":"lnbc...","network":"mainnet","paymentHash":""}}`. Echo it byte for byte. | | `expires` | RFC 3339 UTC. Never later than the BOLT11 invoice expiry. A modern credential must be redeemed before this instant. | | `digest` | Present only on proxied `POST` / `PUT` / `PATCH` requests with a body: the RFC 9530 `Content-Digest` of that body, bound into `id`. Resend the identical body when you redeem. | | `invoice`, `amount`, `currency` | Legacy parameters for clients that predate the drafts. Same invoice as `request.methodDetails.invoice`. | Send `Accept-Payment: lightning/charge;q=0` on the initial request if you want the `Payment` header suppressed and only the L402 header returned. ### Redeem with a modern credential Pay the invoice, then build the credential the drafts describe: a JSON object with the challenge you received (echoed exactly, including `request`) and a `payload` holding the preimage as 64 lowercase hex characters. base64url-encode it and send it as a bearer token. ```json { "challenge": { "id": "k9Q3...", "realm": "lightning-enable", "method": "lightning", "intent": "charge", "request": "eyJhbW91bnQiOiIxMDAiLC...", "expires": "2026-09-11T18:30:00Z" }, "payload": { "preimage": "7f8a9b2c...e9f0a" } } ``` ```http GET /l402/proxy/abc123/api/data HTTP/1.1 Authorization: Payment eyJjaGFsbGVuZ2UiOnsiaWQiOiJrOVEz... ``` On success the response includes a receipt you can store or forward: ```http HTTP/1.1 200 OK Payment-Receipt: eyJjaGFsbGVuZ2VJZCI6Ims5UTMuLi4iLCJtZXRob2QiOiJsaWdodG5pbmciLC... Cache-Control: private ``` The receipt decodes to `{"challengeId":"k9Q3...","method":"lightning","reference":"","status":"success","timestamp":"2026-09-11T18:12:04Z"}`. The `reference` is the payment hash, never the preimage. **Single use.** A modern credential is consumed atomically the first time it verifies. A second request with the same token gets a fresh 402. If the server accepted the credential but failed to deliver the upstream response, the consumption is released so you can retry with the same token. Redeem before `expires`. Classic L402 and legacy `Payment` credentials keep their reuse-until-expiry behaviour; nothing about them changed. ### Redeem with a legacy `Payment` credential ```http Authorization: Payment method="lightning", preimage="7f8a9b2c...e9f0a" ``` No macaroon and no challenge echo. The server matches the preimage to a challenge it minted for that resource and price. Reusable until the challenge expires. ### Modern-path errors Failures on the modern path return `402` with `application/problem+json`, a fresh challenge in `WWW-Authenticate`, and `Cache-Control: no-store`. The `type` is `https://paymentauth.org/problems/`: | `type` slug | Meaning | |---|---| | `invalid-challenge` | The echoed challenge doesn't decode, has a currency other than `sat`, or is missing its expiry or payment hash. | | `verification-failed` | The preimage doesn't hash to the challenge's payment hash, the challenge belongs to another merchant, the credential is past `expires`, or it was already redeemed (single use). The `detail` member says which. | | `payment-insufficient` | The challenge amount doesn't match the resource's price. | | `method-unsupported` | Modern credentials are switched off on this server. Fall back to L402. | A malformed bearer token is rejected outright. It is never reinterpreted as a legacy credential. ### Verify credentials as a producer If you mint challenges with the [Producer API](#l402-producer-api), verify each profile with the matching endpoint: - Classic L402 and legacy `Payment` credentials: [`POST /api/l402/challenges/verify`](#verify-l402-token). - Modern bearer credentials: [`POST /api/l402/challenges/verify-credential`](#verify-payment-credential-single-use), which consumes the token and returns a ready-to-serve `Payment-Receipt` value. --- ## L402 Producer API The Producer API lets merchants create L402 challenges programmatically — enabling agent-to-agent commerce where your AI agent charges other agents for access to resources. :::info Plans Every plan can mint challenges. The **Free Producer Sandbox** is capped at 3 endpoints, 200 challenges a month, and 1,000 sats per challenge; **Agentic Commerce** ($49/mo) lifts the caps. Paying for L402 resources needs no Lightning Enable account at all. ::: :::tip Agents That Earn This is the supply side of agentic commerce. Your agent creates L402 challenges; other agents pay them. See the [L402 Producer API guide](/products/agentic-commerce/l402-producer-api) for the full walkthrough. ::: ### Create L402 Challenge Create a Lightning invoice + macaroon challenge for a resource. ```http POST /api/l402/challenges ``` #### Headers | Header | Required | Description | |--------|----------|-------------| | `X-API-Key` | Yes | Merchant API key | | `Idempotency-Key` | No | Retry-safe key, max 200 characters. The same key with the same resource and price returns the same challenge for the life of that invoice; a different resource or price under the same key is a `409` while that invoice is still live, and a fresh mint once it has expired. See [Idempotency](/products/agentic-commerce/producer-api-reference#idempotency). | | `X-Idempotency-Key` | No | The spelling this API shipped with. Identical behaviour; `Idempotency-Key` wins if you send both. | #### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `resource` | string | Yes | Resource identifier (URL, service name, or description) | | `priceSats` | long | Yes | Price in satoshis (minimum 1) | | `description` | string | No | Description shown on the Lightning invoice | | `idempotencyKey` | string | No | Same meaning as the `Idempotency-Key` header, for clients that can't set headers | #### Example ```bash curl -X POST https://api.lightningenable.com/api/l402/challenges \ -H "X-API-Key: YOUR_MERCHANT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "resource": "/api/weather/forecast", "priceSats": 50, "description": "7-day weather forecast" }' ``` #### Response (200 OK) ```json { "invoice": "lnbc500n1p3xyza...", "macaroon": "AgELbGlnaHRuaW5n...", "paymentHash": "abc123def456...", "expiresAt": "2026-03-13T14:30:00Z", "resource": "/api/weather/forecast", "priceSats": 50, "mppChallenge": "id=\"k9Q3...\", realm=\"lightning-enable\", method=\"lightning\", intent=\"charge\", ..." } ``` `mppChallenge` is the `Payment` header value for the same invoice: send `WWW-Authenticate: Payment ` beside your L402 header so MPP clients can pay you too. It's `null` when MPP is switched off. #### Error Responses Returned as `application/problem+json` with a stable `type` URI. See [Error format](/products/agentic-commerce/producer-api-reference#error-format) for the full body and the complete code list. | Status | Description | |--------|-------------| | 400 | Invalid body (`invalid_request`), a reserved `renew:` or `signup:` resource, a blank or over-long idempotency key, or no payment provider credential on your account (`payment_provider_not_configured`) | | 401 | Missing or invalid API key | | 402 | A plan cap was reached: `plan_price_limit`, `plan_volume_limit`, or `plan_endpoint_limit` | | 403 | L402 not enabled on your account (`l402_not_enabled`) | | 409 | `idempotency_key_reuse` (the key is bound to a different live resource or price), or `endpoint_retired` | | 415 | No JSON body, or a `Content-Type` other than `application/json` | | 502 | Your payment provider refused the invoice (`provider_rejected`), usually a key problem | | 503 | The provider is down (`provider_unavailable`), your NWC wallet is busy (`payment_provider_busy`), or the challenge couldn't be recorded (`challenge_persist_failed`). Nothing was invoiced | --- ### List Your Challenges List the challenges you have minted, with payment status. Scoped to the account behind your API key. ```http GET /api/l402/challenges?status=paid&since=2026-09-01T00:00:00Z&limit=50&offset=0 ``` | Query parameter | Default | Description | |---|---|---| | `status` | none | `paid`, `unpaid`, or `expired` | | `since` | none | ISO 8601 lower bound on `createdAt` | | `limit` | 50 | Clamped to 1..200 | | `offset` | 0 | Clamped to at least 0 | Returns `{ challenges, total, limit, offset, status, since }`; the unpaged total is also in the `X-Total-Count` header. Each challenge carries `paymentHash`, `resource`, `amountSats`, `status`, `createdAt`, `paidAt`, `expiresAt`, and `idempotencyKey` — never a macaroon or a preimage. ```bash curl "https://api.lightningenable.com/api/l402/challenges?status=paid&limit=25" \ -H "X-API-Key: YOUR_MERCHANT_API_KEY" ``` --- ### Get One Challenge ```http GET /api/l402/challenges/{paymentHash} ``` Returns the same object as one element of the list. A payment hash belonging to another account returns `404`, not `403`. --- ### Verify L402 Token Verify a classic L402 token (macaroon + preimage), or a legacy `Payment` credential (preimage only). ```http POST /api/l402/challenges/verify ``` #### Headers | Header | Required | Description | |--------|----------|-------------| | `X-API-Key` | Yes | Merchant API key | #### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `macaroon` | string | For L402 | Base64-encoded macaroon. Omit it to verify a preimage-only `Payment` credential | | `preimage` | string | Yes | Hex-encoded preimage (64 characters) | | `resource` | string | No | Reject unless the challenge was minted for this resource | #### Example ```bash curl -X POST https://api.lightningenable.com/api/l402/challenges/verify \ -H "X-API-Key: YOUR_MERCHANT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "macaroon": "AgELbGlnaHRuaW5n...", "preimage": "7f8a9b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a" }' ``` #### Response (200 OK — valid) ```json { "valid": true, "resource": "/api/weather/forecast", "merchantId": 42, "amountSats": 50, "paymentHash": "abc123def456..." } ``` #### Response (200 OK — invalid) ```json { "valid": false, "error": "Preimage does not match payment hash" } ``` :::note Verifying marks the challenge paid The first successful verification of a credential records the underlying challenge as paid — it starts showing as `status: "paid"` in the listing above, and fires the `l402.challenge.paid` webhook to your callback URL if you have one configured. Later verifications of the same credential do not re-fire it. See [Payment webhooks](/products/agentic-commerce/producer-api-reference#payment-webhooks). ::: --- ### Verify Payment Credential (single-use) Verify a modern `Payment` bearer credential (the base64url JSON token described in [Payment (MPP) credentials](#payment-mpp-credentials)). Verifying consumes the credential: a second call with the same token returns `valid: false`. ```http POST /api/l402/challenges/verify-credential ``` #### Headers | Header | Required | Description | |--------|----------|-------------| | `X-API-Key` | Yes | Merchant API key | #### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `credential` | string | Yes | The token exactly as received, with or without the leading `Payment ` scheme word | | `resource` | string | No | Reject unless the challenge was minted for this resource | | `amountSats` | integer | No | Reject unless the challenge was minted for this price | #### Example ```bash curl -X POST https://api.lightningenable.com/api/l402/challenges/verify-credential \ -H "X-API-Key: YOUR_MERCHANT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "credential": "", "resource": "/api/weather/forecast", "amountSats": 50 }' ``` #### Response (200 OK — valid) ```json { "valid": true, "consumed": true, "resource": "/api/weather/forecast", "merchantId": 42, "amountSats": 50, "paymentHash": "abc123def456...", "receipt": "eyJjaGFsbGVuZ2VJZCI6Ims5UTMuLi4iLC..." } ``` Serve `receipt` back to the payer as the `Payment-Receipt` response header on the resource you gate. #### Response (200 OK — invalid) ```json { "valid": false, "consumed": false, "error": "Preimage does not match the challenge payment hash." } ``` `400 mpp_not_supported` means modern credentials are switched off on the server. Like `/verify`, the first successful call marks the challenge paid and fires `l402.challenge.paid`. --- ## Client Libraries Use an L402 client rather than hand-rolling the 402, pay, retry loop. Each one pays the invoice from your wallet, caches the credential per path, and retries: - **Python**: [`l402-requests`](https://pypi.org/project/l402-requests/) - **JavaScript / TypeScript**: [`l402-requests`](https://www.npmjs.com/package/l402-requests) - **.NET**: [`L402Requests`](https://www.nuget.org/packages/L402Requests) - **AI agents**: the [Lightning Enable MCP server](https://github.com/refined-element/lightning-enable-mcp) By hand with curl: ```bash # 1. Get the challenge curl -s https://api.lightningenable.com/l402/proxy/my-api/data > challenge.json MACAROON=$(jq -r '.l402.macaroon' challenge.json) INVOICE=$(jq -r '.l402.invoice' challenge.json) # 2. Pay $INVOICE with your wallet and copy the preimage read -p "Preimage: " PREIMAGE # 3. Retry with the credential curl https://api.lightningenable.com/l402/proxy/my-api/data \ -H "Authorization: L402 $MACAROON:$PREIMAGE" ``` In a browser with a WebLN wallet, `const { preimage } = await window.webln.sendPayment(invoice);` returns the preimage. ## Next Steps - [Proxy Configuration](/products/agentic-commerce/proxy-configuration) - Set up L402 proxies - [How It Works](/products/agentic-commerce/how-it-works) - Technical deep dive - [API Monetization](/products/agentic-commerce/api-monetization) - Protect your APIs ============================================================================== # Merchant Settings Source: https://docs.lightningenable.com/api-reference/merchant-settings ============================================================================== # Merchant Settings API The Merchant Settings API allows you to manage your own account configuration. All endpoints require authentication with your merchant API key. ## Endpoints | Method | Endpoint | Description | |--------|----------|-------------| | `GET` | `/api/merchant/me` | Get account info and onboarding status | | `GET` | `/api/merchant/l402-status` | Check L402 license status | | `PUT` | `/api/merchant/strike-key` | Update Strike API key (Strike merchants) | | `PUT` | `/api/merchant/opennode-key` | Update OpenNode API key (OpenNode merchants) | | `PUT` | `/api/merchant/nwc-connection` | Save a Nostr Wallet Connect connection string (your own wallet) | | `PUT` | `/api/merchant/payment-provider` | Set the active payment provider (`strike`, `opennode` or `nwc`) | | `PUT` | `/api/merchant/webhook-url` | Update webhook URL and secret | | `POST` | `/api/merchant/webhook-secret/rotate` | Generate a new webhook signing secret (returned once) | | `GET` | `/api/merchant/subscription` | Get subscription details | | `GET` | `/api/merchant/subscription/renew` | Quote a Lightning (L402) renewal: price, period, eligibility | | `POST` | `/api/merchant/subscription/renew` | Renew Agentic Commerce in sats with an L402 payment | | `POST` | `/api/merchant/validate-strike` | Validate Strike API key | | `POST` | `/api/merchant/validate-opennode` | Validate OpenNode API key | | `GET` | `/api/merchant/api-key-info` | Get metadata about your merchant API key | | `POST` | `/api/merchant/regenerate-key` | Rotate your merchant API key | | `GET` | `/api/merchant/quickstart` | Get interactive onboarding guide | :::note Strike API Key Configuration Strike is the recommended default payment provider and is **self-serve** — configure it yourself with `PUT /api/merchant/strike-key`, then confirm it works with `POST /api/merchant/validate-strike`. No support ticket needed. ::: ## Get Account Info Get your merchant account information and onboarding status. ```http GET /api/merchant/me ``` ### Response ```json { "merchantId": 123, "name": "My Company", "email": "api@mycompany.com", "planTier": "individual", "subscriptionStatus": "active", "isActive": true, "createdAt": "2024-01-15T10:30:00Z", "features": { "refundsEnabled": false, "multiCurrencyEnabled": true, "analyticsEnabled": true, "prioritySupport": true, "customBrandingEnabled": false, "maxWebhookEndpoints": 5, "l402Enabled": true }, "onboarding": { "hasOpenNodeKey": false, "hasStrikeKey": true, "hasNwcConnection": false, "hasWebhookUrl": true, "hasWebhookSecret": true, "hasActiveProxy": true, "proxyCount": 2, "isFullyConfigured": true } } ``` `features` reflects your plan, except `refundsEnabled` and `l402Enabled`, which are per-account flags. `onboarding.isFullyConfigured` is `true` when the account has a usable credential for its payment provider, the same check every invoice and challenge goes through. ### Example ```bash curl https://api.lightningenable.com/api/merchant/me \ -H "X-API-Key: YOUR_API_KEY" ``` ## Check L402 License Status Check if your account has L402 features enabled — useful for dashboards and integrations that want to show a merchant's L402 capability. ```http GET /api/merchant/l402-status ``` ### Response (L402 Enabled) ```json { "l402Enabled": true, "planTier": "l402", "subscriptionStatus": "active", "isActive": true } ``` ### Response (L402 Not Enabled) ```json { "l402Enabled": false, "planTier": "individual", "subscriptionStatus": "active", "isActive": true } ``` `l402Enabled` is a per-account flag, not a plan lookup. All three tiers include L402, so `false` on an active account means the flag was never applied (most often on an account created under a retired tier id). Contact support to have it applied; buying the plan again won't set it. It also reads `false` whenever `isActive` is `false`. ### Response Fields | Field | Type | Description | |-------|------|-------------| | `l402Enabled` | boolean | Whether L402 features are available | | `planTier` | string | Current plan tier (see mapping table below) | | `subscriptionStatus` | string | Subscription status, for example `active`, `trialing`, `past_due`, `canceled`; `unknown` when none is recorded | | `isActive` | boolean | Whether the merchant account is active | ### Plan Tier Name Mapping The `planTier` field uses internal code names. There are three, cheapest first: | Internal `planTier` Value | User-Facing Product Name | Price | |---------------------------|--------------------------|-------| | `free` | Free Producer Sandbox | $0 | | `individual` | Agentic Commerce | $49/mo | | `l402` | Agentic Commerce — Business | Contact us | ### Plan Tiers and L402 Support | Plan | Internal Value | Price | L402 Enabled | Trial Eligible | |------|---------------|-------|--------------|----------------| | Free Producer Sandbox | `free` | $0 | ✅ Yes — capped at 3 endpoints, 200 challenges/mo, 1,000 sats per challenge | ❌ No — Free is the floor, not a trial | | Agentic Commerce | `individual` | $49/mo | ✅ Yes | ✅ Yes | | Agentic Commerce — Business | `l402` | Contact us | ✅ Yes | Arranged directly on contact, not via self-serve checkout | :::info `planTier` is normalized — changed September 2026 `planTier` is the **resolved** tier, not the raw stored column. `GET /api/merchant/me`, `GET /api/merchant/l402-status`, and `GET /api/merchant/subscription` all return one of the three values above. If your account was created before September 2026 under a tier id that has since been retired — `standard`, `kenticocommerce`, `standalone`, `standaloneapi`, or `pilot` — the field reports the live tier that id maps to, not the id you may have stored: the retired paid ids report as `individual`, and `pilot` reports as `free`. Update any client that string-matches on a retired id, and treat an unrecognized value as Free. ::: ### Example ```bash curl https://api.lightningenable.com/api/merchant/l402-status \ -H "X-API-Key: YOUR_API_KEY" ``` :::tip MCP Integration The MCP server's paying tools need only a wallet and never call this endpoint. Set `LIGHTNING_ENABLE_API_KEY` to your merchant API key to unlock its producer and agent-service publishing tools. ::: ## Update Strike API Key Configure your Strike API key yourself — Strike is the recommended default and fully self-serve. Saving the key also defaults your account to the Strike provider on first save (it never overwrites an explicit prior provider choice). ```http PUT /api/merchant/strike-key ``` ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `strikeApiKey` | string | Yes | Your Strike API key (needs `partner.receive-request.create` and related scopes) | ### Request ```json { "strikeApiKey": "your-strike-api-key" } ``` ### Response ```json { "success": true, "message": "Strike API key updated successfully. Webhook will re-register on the next L402 challenge." } ``` ### Example ```bash curl -X PUT https://api.lightningenable.com/api/merchant/strike-key \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "strikeApiKey": "your-strike-api-key" }' ``` After saving, confirm the key works with `POST /api/merchant/validate-strike`. ## Save a Nostr Wallet Connect Connection Settle payments into your own Lightning wallet instead of a provider account. Copy a Nostr Wallet Connect connection string from your wallet app and save it here. Lightning Enable checks the string's format before storing it, and never returns it on any endpoint. If you haven't chosen a provider yet, this also sets your provider to `nwc`. See [NWC setup](/nwc-setup/account-setup). ```http PUT /api/merchant/nwc-connection ``` ```json { "nwcConnectionString": "nostr+walletconnect://?relay=wss://&secret=" } ``` A malformed string gets `400` with an `error` naming the bad part. On success: ```json { "success": true, "message": "Nostr Wallet Connect connection saved. ...", "nwcConnectionString": "" } ``` The NWC lane receives Lightning payments only: no refunds, on-chain addresses, or fiat currencies. To check it works, mint a challenge with `POST /api/l402/challenges`; there is no separate validate endpoint. ## Set Payment Provider Choose which credential your invoices are created with. Values are lowercase: `strike`, `opennode`, or `nwc`; anything else is `400`. ```http PUT /api/merchant/payment-provider ``` ```json { "provider": "strike" } ``` ```json { "success": true, "message": "Payment provider updated to strike." } ``` Without an explicit choice, an account holding exactly one credential uses that one; otherwise the platform default (Strike) applies. ## Update OpenNode API Key Configure your OpenNode API key. This endpoint applies to merchants using OpenNode as their payment provider. Strike merchants should use `PUT /api/merchant/strike-key` instead (self-serve — no support ticket required). ```http PUT /api/merchant/opennode-key ``` ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `openNodeApiKey` | string | Yes | Your OpenNode API key | :::tip Get Your OpenNode API Key https://app.opennode.com → Settings → API Keys. Use a production key; the hosted platform doesn't accept OpenNode dev keys. ::: ### Request ```json { "openNodeApiKey": "your-opennode-api-key" } ``` ### Response ```json { "success": true, "message": "OpenNode API key updated successfully. You can now create L402 proxies." } ``` ### Example ```bash curl -X PUT https://api.lightningenable.com/api/merchant/opennode-key \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "openNodeApiKey": "your-opennode-api-key" }' ``` ## Update Webhook URL Configure where Lightning Enable should send webhook notifications for payment events. ```http PUT /api/merchant/webhook-url ``` ### Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `webhookUrl` | string | No | Absolute URL to receive webhook notifications. Send an empty value to stop deliveries | | `webhookSecret` | string | No | Secret for the HMAC signature. Omit it to keep the current secret | ### Request ```json { "webhookUrl": "https://mycompany.com/webhooks/lightning", "webhookSecret": "my-webhook-signing-secret" } ``` ### Response ```json { "success": true, "message": "Webhook settings updated successfully." } ``` ### Example ```bash curl -X PUT https://api.lightningenable.com/api/merchant/webhook-url \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "webhookUrl": "https://mycompany.com/webhooks/lightning", "webhookSecret": "my-webhook-signing-secret" }' ``` ## Rotate Webhook Secret Generate a new secret for the HMAC signature (`X-LightningEnable-Signature`) on webhook deliveries. The merchant API returns the secret only in this response, the same way `POST /api/merchant/regenerate-key` returns a new API key once. To get a value again, rotate again. ```http POST /api/merchant/webhook-secret/rotate ``` No request body. Rate limit: the per-merchant `merchant-key` policy (see [Rate Limiting](./rate-limiting)). ### Response ```json { "webhookSecret": "", "message": "Webhook secret rotated. Store it now: it is not readable again through the merchant API; rotate to get a new one. Every delivery from now on, including retries of earlier events, is signed with this secret." } ``` The secret is 32 random bytes, base64-encoded (44 characters), the same format signup generates. `GET /api/merchant/me` reports only whether a secret is on file (`onboarding.hasWebhookSecret`). Your account already has a secret from signup, but the merchant API doesn't return it. Call this endpoint once to get a value you can verify deliveries with. **The old secret stops working immediately.** Deliveries are signed with the secret on file when they are sent, so retries of events queued before the rotation are also signed with the new secret. Update your receiver right away. **Each call replaces the secret.** If two rotate calls overlap, the response of the last successful call holds the live secret. Rotate once and store the response before you send another call. ### Example ```bash curl -X POST https://api.lightningenable.com/api/merchant/webhook-secret/rotate \ -H "X-API-Key: le_merchant_abc123" ``` ## Get Subscription Details Get your current subscription plan and limits, how the plan is paid, the date it's paid through, whether you can renew with sats over L402, and your recent L402 renewal receipts. ```http GET /api/merchant/subscription ``` ### Response This example shows an account renewed with sats over L402. Sats amounts are shown as placeholders; a real response carries integers, and the live price is the `priceSats` that the [renewal quote](#get-quote) returns. ```json { "planTier": "individual", "planName": "Agentic Commerce", "status": "active", "stripeCustomerId": null, "limits": { "maxMerchants": 1, "maxEnvironments": 2, "maxWebhookEndpoints": 5 }, "paymentMethod": "l402", "currentPeriodEnd": "2026-11-10T14:03:22Z", "renewal": { "eligible": true, "reason": null, "priceSats": , "periodDays": 30, "endpoint": "/api/merchant/subscription/renew" }, "payments": [ { "paymentHash": "9f2c4e7a1b3d5f60718293a4b5c6d7e8f9012a3b4c5d6e7f8091a2b3c4d5e6f7a", "amountSats": , "periodStart": "2026-10-11T14:03:22Z", "periodEnd": "2026-11-10T14:03:22Z", "createdAt": "2026-10-11T14:03:22Z", "channel": "mcp-dotnet" } ] } ``` - `paymentMethod` is `bitcoin` for an account paid through the legacy Bitcoin checkout, `l402` for an account paid in sats, and `null` otherwise. Card accounts report `null` (some older rows may carry `stripe`), and so do Free Producer Sandbox and Fast Lane trial accounts. - `renewal.reason` is `null` when `renewal.eligible` is `true`. Otherwise it names why a sats renewal isn't offered — for example, `StripeSubscriptionActive` for an account billed by card. - `payments` lists up to the 12 most recent L402 renewal receipts, newest first. It's empty for an account billed by card. - `payments[].channel` classifies the `User-Agent` header of the request that redeemed the payment, by prefix (case-insensitive): `mcp-dotnet` (starts with `LightningEnable-MCP/`), `mcp-python` (starts with `LightningEnable-MCP-Python/`), `l402-client` (starts with `l402-requests`, `L402Requests`, or `l402-ts`), `unknown` (no `User-Agent` header, and receipts recorded before this field existed), or `other` (any other `User-Agent`). A client lands in a named channel only once it sends that header; the Lightning Enable MCP server sends it from version 2.1.0. The raw header isn't returned. ### Subscription Status Values | Status | Description | |--------|-------------| | `active` | Subscription is active and paid | | `trialing` | In the 30-day trial (card checkout or L402 Fast Lane). Full Agentic Commerce access | | `past_due` | Payment failed, grace period | | `canceled` | Subscription was canceled | ### Example ```bash curl https://api.lightningenable.com/api/merchant/subscription \ -H "X-API-Key: YOUR_API_KEY" ``` ## Renew with Lightning (L402) Pay for Agentic Commerce in sats instead of by card. One payment adds one period (`periodDays`) at the sats price in `priceSats`. Read both from the quote, `GET /api/merchant/subscription/renew`, rather than from this page: the sats price is configuration and can change; a challenge you already hold is honored at the price it was issued at unless the price has gone up, in which case mint a fresh one. `GET` quotes the renewal with no side effects. `POST` without an `Authorization` header returns an L402 challenge; `POST` with `Authorization: L402 :` credits the period. For a walkthrough with the MCP tool and the `l402-requests` client, see [Renew in sats](/getting-started/activate-with-lightning#renew-in-sats). ```http GET /api/merchant/subscription/renew POST /api/merchant/subscription/renew ``` ### Headers | Header | Required | Description | |--------|----------|-------------| | `X-API-Key` | Yes, on every call | Your merchant API key. The renewal is for the account this key belongs to. | | `Authorization` | `POST` completion only | `L402 :`. Omit it to get a challenge. | Neither method takes a request body. The endpoint is exempt from subscription enforcement, so a lapsed account can reach it. **Rate limits**, per API key: `GET` shares your per-merchant budget. `POST` without `Authorization` (a new challenge) is limited to 5 a minute, because each one opens a new invoice. `POST` with `Authorization` (redeeming a token) has a separate allowance of 60 a minute, so requesting challenges never blocks a token you've paid. Over either limit, the endpoint answers `429`. ### Who can renew A Free Producer Sandbox account, a Fast Lane trial account, a sats-paid account (running or lapsed), an account whose card subscription is canceled (Stripe status `canceled` or `incomplete_expired`), and an account paid through the legacy Bitcoin checkout while its Bitcoin period is still running can renew. When that Bitcoin period lapses, the legacy job deactivates the account, so its API key gets `401` here; email support@lightningenable.com to restore it. An account with a card subscription that isn't canceled (any other status, including `trialing`, `past_due`, `unpaid`, `incomplete`, `paused`, or no recorded status), an Agentic Commerce — Business account, and an operator-disabled account can't. A card subscription that isn't canceled can still charge the card, so a sats renewal alongside it would bill you twice. ### Fields | Field | Type | Meaning | |-------|------|---------| | `priceSats` | integer | Sats for one period at the time of the call | | `periodDays` | integer | Days one payment adds | | `paymentHash` | string | Hex payment hash of the challenge; also the receipt ID | | `expiresAt` | string (ISO 8601) | When the invoice expires | | `currentPeriodEnd` | string or null | On `GET`, the `402`, and a `409`: the account's effective paid-through date, the later of the card/sats period end and a legacy Bitcoin expiry; can be in the past (for example, a lapsed Fast Lane trial). On `200`, the paid-through date this payment produced | | `periodEndIfPaid` | string (ISO 8601) | The paid-through date this payment produces | | `eligible` | boolean | On `GET` only. When `false`, `reason` says why | | `reason` | string or null | On `GET` only: `StripeSubscriptionActive`, `BusinessPlanContactSupport`, `PrepaidCapReached`, `ManuallyDisabled`, or `Disabled`; `null` when eligible | | `paymentMethod` | string or null | On `GET` only: `bitcoin` (legacy Bitcoin checkout), `l402` (sats), or `null` (card accounts, plus Free and trial accounts; some older card rows carry `stripe`) | | `endpoint` | string | On `GET` only: the renew endpoint path | | `paidSats` | integer | On `200`: the sats credited | | `alreadyCredited` | boolean | On `200`: `true` when this token was credited before | | `payments[]` | array | On `GET /api/merchant/subscription`: the last 12 receipts, each with `paymentHash`, `amountSats`, `periodStart`, `periodEnd`, `createdAt`, and `channel` | Each payment adds `periodDays` to the later of the call time, `currentPeriodEnd`, and a legacy `bitcoinSubscriptionExpiresAt`. An account already paid more than 365 days ahead can't get a new challenge until it's within that window. The cap applies when you request a challenge, not when you redeem one: a token you've already paid is credited even if the account has since moved past 365 days. A renewal token stays redeemable for 7 days after its challenge is issued, so a token you paid survives a temporary refusal (a `409` or `503`) while you fix its cause. ### GET: quote Returns `200` for every authenticated account, eligible or not. It creates no invoice. Sats amounts in these examples are shown as placeholders such as ``. In a real response they're integers, and the live renewal price is whatever `GET /api/merchant/subscription/renew` returns in `priceSats`. ```json { "eligible": true, "reason": null, "priceSats": , "periodDays": 30, "paymentMethod": "l402", "currentPeriodEnd": "2026-11-10T14:03:22Z", "periodEndIfPaid": "2026-12-10T14:03:22Z", "endpoint": "/api/merchant/subscription/renew" } ``` For an account created through the producer claim endpoint, the quote also carries a `breakdown` block with the per-proxy calculation. See [Renewal quote](/products/agentic-commerce/producer-api-reference#renewal-quote). An account with an active card subscription gets: ```json { "eligible": false, "reason": "StripeSubscriptionActive", "priceSats": , "periodDays": 30, "paymentMethod": null, "currentPeriodEnd": "2026-11-02T09:15:00Z", "periodEndIfPaid": "2026-12-02T09:15:00Z", "endpoint": "/api/merchant/subscription/renew" } ``` ```bash curl https://api.lightningenable.com/api/merchant/subscription/renew \ -H "X-API-Key: YOUR_API_KEY" ``` ### POST: get a challenge Send no `Authorization` header. Eligibility is checked first, so an ineligible account gets its refusal before any invoice exists. Every call mints a fresh challenge with a new invoice; a second call after a credited renewal buys a second period. **`402 Payment Required`** ```http WWW-Authenticate: L402 macaroon="AgEB…", invoice="lnbc…" ``` ```json { "invoice": "lnbc…", "macaroon": "AgEB…", "paymentHash": "9f2c4e7a1b3d5f60718293a4b5c6d7e8f9012a3b4c5d6e7f8091a2b3c4d5e6f7a", "expiresAt": "2026-10-11T15:03:22Z", "priceSats": , "periodDays": 30, "currentPeriodEnd": null, "periodEndIfPaid": "2026-11-10T14:03:22Z", "instructions": "Pay the Lightning invoice, then retry this request with Authorization: L402 :." } ``` ```bash curl -i -X POST https://api.lightningenable.com/api/merchant/subscription/renew \ -H "X-API-Key: YOUR_API_KEY" ``` ### POST: complete the renewal Pay the invoice, then repeat the `POST` with the macaroon and the preimage. ```bash curl -X POST https://api.lightningenable.com/api/merchant/subscription/renew \ -H "X-API-Key: YOUR_API_KEY" \ -H "Authorization: L402 :" ``` **`200 OK`** ```json { "success": true, "alreadyCredited": false, "planTier": "individual", "currentPeriodEnd": "2026-11-10T14:03:22Z", "paidSats": , "paymentHash": "9f2c4e7a1b3d5f60718293a4b5c6d7e8f9012a3b4c5d6e7f8091a2b3c4d5e6f7a" } ``` The account is on `individual` with `paymentMethod: "l402"`, `subscriptionStatus: "active"`, and `currentPeriodEnd` set to the paid-through date. The response never contains the macaroon or the preimage. **Replays.** An L402 token stays valid until it expires (7 days after its challenge was issued), so you can send it again. Each payment hash is credited exactly once. A replay answers `200` with `alreadyCredited: true` and that payment's own period end in `currentPeriodEnd`, which is earlier than the account's paid-through date if a later payment has extended it: ```json { "success": true, "alreadyCredited": true, "planTier": "individual", "currentPeriodEnd": "2026-11-10T14:03:22Z", "paidSats": , "paymentHash": "9f2c4e7a1b3d5f60718293a4b5c6d7e8f9012a3b4c5d6e7f8091a2b3c4d5e6f7a" } ``` Eligibility is checked before the replay, so a replay can get a `409` (for example `stripe_subscription_active`) if the account became ineligible after the payment was credited. That refusal never credits or charges anything. **Price changes.** A token is honored at the price it was minted at, as long as that price is at or above the configured price, so a price decrease never strands a token. After a price increase, a token minted at the old price gets `401 invalid_l402_token`, including on replay. Mint a fresh challenge. An account created through the producer claim endpoint is the exception: its token is honored at the price it was minted at. The token is bound to the account that minted it. Another account's API key can't redeem it. ### Error responses | Status | `error` | When | What to do | |--------|---------|------|------------| | `400` | `invalid_authorization` | `Authorization` isn't `L402 :` | Fix the header, or omit it to get a challenge | | `401` | `invalid_l402_token` | The token is invalid, expired, minted for another account, below the configured price, or doesn't match a renewal challenge | `POST` without `Authorization` for a fresh challenge | | `401` | `API key required` / `Invalid API key` | The `X-API-Key` header is missing or wrong | Send a valid merchant API key | | `403` | `account_disabled` | An operator disabled the account | Contact support | | `409` | `stripe_subscription_active` | The account has a card subscription that isn't canceled (Stripe status other than `canceled` or `incomplete_expired`) | Cancel it in the billing portal first | | `409` | `business_plan_contact_support` | The account is on Agentic Commerce — Business | Contact support | | `409` | `prepaid_cap_reached` | You requested a challenge while the account is paid more than 365 days ahead (never returned when redeeming a paid token) | Wait until the paid-through date is within 365 days | | `429` | — | More than 5 challenge requests (or 60 redeem requests) a minute from this API key | Wait for the window to reset | | `503` | `renewal_contended` | The account was being updated by another request | Retry the same request with the same `Authorization` header after `Retry-After`; it's credited at most once | | `503` | `upstream_unavailable` | The payment provider didn't answer; nothing was invoiced | Retry shortly (honor `Retry-After` when present) | | `503` | `challenge_persist_failed` | The challenge couldn't be recorded; no invoice was issued | Retry in a moment | | `503` | `l402_subscription_unavailable` | Lightning renewals are switched off on this deployment | Pay by card, or retry later | | `503` | `l402_subscription_misconfigured` | Lightning renewals aren't set up on this deployment | Retry later, or contact support | Full bodies for each are in the [error reference](/api-reference/errors#lightning-renewal-errors). ## Validate OpenNode API Key Test your OpenNode API key to verify it's configured correctly. ```http POST /api/merchant/validate-opennode ``` ### Response (Valid Key) ```json { "isValid": true, "message": "OpenNode API key is valid and working." } ``` ### Response (Invalid Key) ```json { "isValid": false, "message": "OpenNode API key validation failed: 401" } ``` ### Response (No Key Configured) ```json { "isValid": false, "message": "No OpenNode API key configured. Use PUT /api/merchant/opennode-key to add one." } ``` ### Example ```bash curl -X POST https://api.lightningenable.com/api/merchant/validate-opennode \ -H "X-API-Key: YOUR_API_KEY" ``` ## Validate Strike API Key Test your Strike API key to verify it's configured correctly. ```http POST /api/merchant/validate-strike ``` Returns `{ "isValid": true|false, "message": "..." }`. Lightning Enable checks that the key can both read and create Strike receive requests; when it can't, `message` names the missing permission. ### Response (No Key Configured) ```json { "isValid": false, "message": "No Strike API key configured. Use PUT /api/merchant/strike-key to add one." } ``` ### Example ```bash curl -X POST https://api.lightningenable.com/api/merchant/validate-strike \ -H "X-API-Key: YOUR_API_KEY" ``` ## Get Quickstart Guide Get a checklist of the setup steps, with what's done. The steps adapt to your payment provider. ```http GET /api/merchant/quickstart ``` ### Response ```json { "merchantId": 123, "merchantName": "My Company", "completedSteps": 3, "totalSteps": 6, "requiredStepsCompleted": 3, "requiredStepsTotal": 3, "isReadyForProduction": true, "steps": [ { "stepNumber": 1, "title": "Configure Strike API Key", "description": "Add your Strike API key so Lightning Enable can create invoices on your behalf.", "endpoint": "PUT /api/merchant/strike-key", "exampleRequest": "{ \"strikeApiKey\": \"your-strike-api-key\" }", "isCompleted": true, "isRequired": true }, { "stepNumber": 2, "title": "Validate Strike Key", "description": "Verify your Strike API key is working correctly.", "endpoint": "POST /api/merchant/validate-strike", "exampleRequest": null, "isCompleted": true, "isRequired": true }, { "stepNumber": 3, "title": "Mint your first challenge (or create a proxy)", "description": "Two ways to satisfy this step: create an L402 proxy configuration pointing to your API, or mint your first challenge directly with POST /api/l402/challenges ...", "endpoint": "POST /api/proxy", "isCompleted": true, "isRequired": true } ] } ``` Steps 4 to 6 (endpoint pricing, testing, documentation) are optional. `isReadyForProduction` is `true` once every required step is done. `endpoint` is always one `METHOD /path`; before you've chosen a provider, an `alternativeEndpoints` array lists the other providers' routes. ### Example ```bash curl https://api.lightningenable.com/api/merchant/quickstart \ -H "X-API-Key: YOUR_API_KEY" ``` ## API Key Info and Rotation `GET /api/merchant/api-key-info` returns `{ maskedApiKey, createdAt, lastRegeneratedAt }`. The key itself is masked to its first 8 and last 4 characters. `POST /api/merchant/regenerate-key` returns `{ newApiKey, regeneratedAt, message }`. **The old key stops working at once.** It's limited to 20 calls a minute. ## Error Responses | Status | `error` | When | |--------|---------|------| | `400` | `Strike API key is required` / `OpenNode API key is required` | Empty key on `PUT /strike-key` or `PUT /opennode-key` | | `400` | `Invalid webhook URL format` | `webhookUrl` isn't an absolute URL | | `400` | `Provider must be 'strike', 'opennode' or 'nwc'` | Unknown value on `PUT /payment-provider` | | `401` | `API key required` / `Invalid API key` | Missing or wrong `X-API-Key` | | `404` | `Merchant not found` | The account behind the key no longer exists | ## Next Steps - [Authentication](/api-reference/authentication) - API key management - [L402 Protocol](/api-reference/l402) - Set up L402 proxy for API monetization - [Webhooks](/api-reference/webhooks) - Configure webhook notifications ============================================================================== # API Overview Source: https://docs.lightningenable.com/api-reference/overview ============================================================================== # API Reference The Lightning Enable API is a REST API for taking Bitcoin Lightning payments and selling API access over L402. Lightning Enable does not hold funds; your payment provider (Strike or OpenNode), or your own wallet over Nostr Wallet Connect, facilitates custody and settlement. ## Base URL ``` https://api.lightningenable.com ``` ## Authentication Authenticated requests carry your merchant API key in the `X-API-Key` header: ```bash curl https://api.lightningenable.com/api/merchant/me \ -H "X-API-Key: YOUR_API_KEY" ``` See [Authentication](/api-reference/authentication), including which endpoints are public. ## API Endpoints ### Payments | Method | Endpoint | Description | |--------|----------|-------------| | `POST` | `/api/payments` | Create payment invoice | | `GET` | `/api/payments/{invoiceId}/status` | **Public** payment status (no API key; safe for browser polling) | | `GET` | `/api/payments/{invoiceId}` | Get payment by ID | | `GET` | `/api/payments/order/{orderId}` | Get payment by order ID | | `POST` | `/api/payments/{invoiceId}/sync` | Re-read status from your payment provider | | `GET` | `/pay/{token}` | **Public** hosted checkout page; the link is returned as `payUrl` | ### Refunds Refunds are enabled per account by support. See [Errors](/api-reference/errors#feature-not-available). | Method | Endpoint | Description | |--------|----------|-------------| | `POST` | `/api/refunds` | Create refund | | `GET` | `/api/refunds/{refundId}` | Get refund status | | `GET` | `/api/refunds` | List refunds | | `GET` | `/api/refunds/invoice/{invoiceId}` | Get refunds for an invoice | | `POST` | `/api/refunds/{refundId}/sync` | Re-read refund status from your payment provider | ### L402 | Method | Endpoint | Description | |--------|----------|-------------| | `POST` | `/api/l402/challenges` | Create an L402 challenge (producer API) | | `GET` | `/api/l402/challenges` | List your challenges with payment status | | `GET` | `/api/l402/challenges/{paymentHash}` | Get one challenge | | `POST` | `/api/l402/challenges/verify` | Verify an L402 or legacy `Payment` credential | | `POST` | `/api/l402/challenges/verify-credential` | Verify a single-use `Payment` credential | | `GET` | `/api/l402/endpoints` | List your L402 endpoints and plan usage | | `*` | `/l402/proxy/{proxyId}/{path}` | L402-protected proxy (public; pays per request) | See [L402 API](/api-reference/l402) and the [Producer API Reference](/products/agentic-commerce/producer-api-reference). ### Webhooks | Method | Endpoint | Description | |--------|----------|-------------| | `POST` | `/api/webhooks/opennode` | OpenNode webhook receiver | | `POST` | `/api/webhooks/strike` | Strike webhook receiver | These receive events **from your payment provider**. Your own notifications go to the callback URL you set with `PUT /api/merchant/webhook-url`. See [Webhooks](/api-reference/webhooks). ### Merchant Settings | Method | Endpoint | Description | |--------|----------|-------------| | `GET` | `/api/merchant/me` | Account info and onboarding status | | `GET` | `/api/merchant/subscription` | Subscription details | | `GET` / `POST` | `/api/merchant/subscription/renew` | Quote or pay a renewal in sats over L402 | | `GET` | `/api/merchant/l402-status` | L402 feature status | | `GET` | `/api/merchant/api-key-info` | API key metadata (masked key, created, last rotated) | | `GET` | `/api/merchant/quickstart` | Onboarding checklist | | `POST` | `/api/merchant/regenerate-key` | Regenerate your API key | | `PUT` | `/api/merchant/payment-provider` | Choose `strike`, `opennode` or `nwc` | | `PUT` | `/api/merchant/strike-key` | Save a Strike API key | | `PUT` | `/api/merchant/opennode-key` | Save an OpenNode API key | | `PUT` | `/api/merchant/nwc-connection` | Save a Nostr Wallet Connect connection string | | `PUT` | `/api/merchant/webhook-url` | Set webhook URL and signing secret | | `POST` | `/api/merchant/webhook-secret/rotate` | Generate a new webhook signing secret (returned once) | | `POST` | `/api/merchant/validate-strike` | Validate your Strike API key | | `POST` | `/api/merchant/validate-opennode` | Validate your OpenNode API key | Full request and response schemas: [Merchant Settings](/api-reference/merchant-settings). ### Health Check | Method | Endpoint | Description | |--------|----------|-------------| | `GET` | `/health` | Service health (public) | ## Request Format | Header | Required | Description | |--------|----------|-------------| | `X-API-Key` | On authenticated endpoints | Your merchant API key | | `Content-Type` | On POST/PUT with a body | `application/json` | | `X-Idempotency-Key` | No | Prevents duplicate payments and refunds on retry | | `Idempotency-Key` | No | Retry-safe key for `POST /api/l402/challenges` | | `X-Correlation-Id` | No | Request tracing ID (generated if you don't send one) | See [Request Headers](/api-reference/headers). ## Errors Every error body has an `error` field. On most endpoints it's a stable snake_case code with a `message` beside it (for example `invalid_expiry`); some older endpoints return a prose `error`, which you should handle by HTTP status. The L402 producer endpoints return `application/problem+json` with a stable `type` URI. Model-binding failures return the standard ASP.NET validation problem (a `400` with an `errors` dictionary). See [Errors](/api-reference/errors). ## HTTP Status Codes | Code | Description | |------|-------------| | `200` | Success | | `201` | Created | | `400` | Bad request: fix the request | | `401` | Missing or invalid API key | | `402` | Payment required (L402), or an L402 plan cap was reached | | `403` | Subscription or feature gate | | `404` | Not found (also for another account's resource) | | `409` | Conflict with the current state | | `429` | Rate limited; honor `Retry-After` | | `500` | Server error; the body carries a `correlationId` | | `502` | The payment provider or proxied API returned an error | | `503` | Temporarily unavailable; retry | ## Pagination `GET /api/refunds` takes `skip` (default 0) and `take` (default 50, max 100). The L402 listings use their own parameters and return the unpaged total in `X-Total-Count`; see [L402 API](/api-reference/l402#list-your-challenges). ## Rate Limiting Every request counts against a global 100 per minute (per API key, or per IP without one), and most routes also have a named policy, for example 10 per minute per API key for `POST /api/payments`. See [Rate Limiting](/api-reference/rate-limiting). ## SDKs and Libraries L402 client libraries (HTTP clients that pay 402s for you): - **.NET / C#**: [`L402Requests`](https://www.nuget.org/packages/L402Requests) on NuGet - **JavaScript / TypeScript**: [`l402-requests`](https://www.npmjs.com/package/l402-requests) on npm - **Python**: [`l402-requests`](https://pypi.org/project/l402-requests/) on PyPI Agent SDKs (higher-level agent commerce primitives): - **.NET / C#**: [`LightningEnable.AgentSdk`](https://www.nuget.org/packages/LightningEnable.AgentSdk) on NuGet - **JavaScript / TypeScript**: [`le-agent-sdk`](https://www.npmjs.com/package/le-agent-sdk) on npm - **Python**: [`le-agent-sdk`](https://pypi.org/project/le-agent-sdk/) on PyPI For MCP clients (Claude Desktop, Claude Code and others), see the [Lightning Enable MCP](https://github.com/refined-element/lightning-enable-mcp) repository. ## Quick Examples ### Create Payment ```bash curl -X POST https://api.lightningenable.com/api/payments \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "orderId": "ORDER-12345", "amount": 49.99, "currency": "USD", "description": "Premium Subscription" }' ``` ### Check Payment Status ```bash curl https://api.lightningenable.com/api/payments/1042 \ -H "X-API-Key: YOUR_API_KEY" ``` ## Health Check Endpoint `GET /health` is public and returns the status of the API and its dependencies, so you can point an uptime monitor or load-balancer probe at it. ```bash curl https://api.lightningenable.com/health ``` ```json { "status": "Healthy", "totalDuration": 42.15, "checks": [ { "name": "database", "status": "Healthy", "duration": 38.72, "description": null, "exception": null, "tags": ["db", "sql"] }, { "name": "store-tenant", "status": "Healthy", "duration": 3.1, "description": null, "exception": null, "tags": ["store"] } ] } ``` | Field | Description | |-------|-------------| | `status` | Overall result: `Healthy`, `Degraded`, or `Unhealthy` | | `totalDuration` | Time to run all checks, in milliseconds | | `checks[]` | One entry per check, with `name`, `status`, `duration`, `description`, `exception`, and `tags` | | Check | What it verifies | |-------|------------------| | `database` | The API can query its database. A failure makes the whole response `Unhealthy` | | `store-tenant` | Store API configuration. It reports `Degraded`, never `Unhealthy` | The response is `200` for `Healthy` and `Degraded`, and `503` for `Unhealthy`. Alert on a non-`200` status, or on any check whose `status` isn't `Healthy`. ## Next Steps - [Authentication](/api-reference/authentication) - API key management - [Request Headers](/api-reference/headers) - Idempotency, correlation IDs, and API versioning - [Payments](/api-reference/payments) - Create and manage payments - [Webhooks](/api-reference/webhooks) - Real-time notifications - [Errors](/api-reference/errors) - Error handling ============================================================================== # Payments API Source: https://docs.lightningenable.com/api-reference/payments ============================================================================== # Payments API Create Lightning invoices and manage payment status. ## Create Payment Create a new Lightning invoice for a customer payment. ```http POST /api/payments ``` ### Request Headers | Header | Required | Description | |--------|----------|-------------| | `X-API-Key` | Yes | Your merchant API key | | `Content-Type` | Yes | `application/json` | | `X-Idempotency-Key` | No | Makes a retry return the first response instead of a second invoice. See [Request Headers](/api-reference/headers#x-idempotency-key) | ### Request Body ```json { "orderId": "ORDER-12345", "amount": 49.99, "currency": "USD", "description": "Premium Subscription", "customerEmail": "customer@example.com", "expiresInSeconds": 900, "metadata": { "productId": "prod_123", "userId": "user_456" } } ``` ### Parameters | Field | Type | Required | Description | |-------|------|----------|-------------| | `orderId` | string | Yes | Your unique order identifier (1–100 chars) | | `amount` | decimal | Yes | Payment amount in `currency`. **`0` creates a demo invoice** that auto-confirms without real payment | | `currency` | string | Yes | Uppercase 3-letter code (`USD`, `EUR`, `GBP`, `BTC`) — anything else fails validation | | `description` | string | No | Payment description (max 500 chars) | | `customerEmail` | string | No | Customer email for receipt | | `customerName` | string | No | Customer name (max 200 chars) | | `successUrl` | string | No | URL to send the customer to after payment | | `metadata` | object | No | Custom key-value data — **string values only** | | `expiresInSeconds` | integer | No | How long the invoice stays payable, from `60` to `86400` (1 minute to 24 hours). Leave it out to use your payment provider's default lifetime (about 24 hours on Strike). A value outside the range, or one that isn't a whole number, is `400 invalid_expiry`. See [Invoice Expiration](#invoice-expiration) | ### Response ```json { "invoiceId": "1042", "status": "unpaid", "amount": 49.99, "currency": "USD", "lightningInvoice": "lnbc1250000n1pnxyz...", "paymentHash": "a1b2c3d4e5f6...", "onchainAddress": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh", "hostedCheckoutUrl": "https://checkout.opennode.com/abc123", "payUrl": "https://api.lightningenable.com/pay/YOUR_PAYMENT_TOKEN", "providerChargeId": "charge_abc123", "openNodeChargeId": "charge_abc123", "createdAt": "2026-07-03T12:00:00Z", "expiresAt": "2026-07-03T13:00:00Z" } ``` | Field | Notes | |-------|-------| | `invoiceId` | Lightning Enable invoice ID — a numeric string (e.g. `"1042"`). Use it for status lookups | | `lightningInvoice` | BOLT11 invoice to display / encode as a QR code | | `paymentHash` | The invoice's payment hash — use this instead of parsing the BOLT11 yourself | | `onchainAddress` | On-chain fallback address, when the provider supplies one | | `hostedCheckoutUrl` | Provider-hosted checkout page — **OpenNode only**; empty for Strike merchants | | `payUrl` | Lightning Enable's own hosted checkout page for this invoice. **Treat it as a secret** — see below | | `expiresAt` | When the invoice stops being payable, read from the BOLT11 invoice itself (its creation time plus its expiry). Always honor this field (see [Invoice Expiration](#invoice-expiration)) | :::caution `payUrl` is a payment link, not a public URL The path segment after `/pay/` is a random per-invoice token, and holding it is what authorizes the checkout page: anyone with the URL can see that invoice's amount, description, merchant name and BOLT11 invoice. Send it to the buyer, and keep it out of logs, referrers and crawlable pages — the same care you would give a payment link from any processor. It cannot be derived from `invoiceId`, and `/pay/1042` does not resolve. If you lose the URL, read `payUrl` back from `GET /api/payments/{invoiceId}` with your API key. ::: :::note Provider Charge IDs `providerChargeId` is the provider-agnostic charge identifier that works with both Strike and OpenNode. `openNodeChargeId` is retained for backward compatibility and will always be populated alongside `providerChargeId`. ::: ### Example ```bash curl -X POST https://api.lightningenable.com/api/payments \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "orderId": "ORDER-12345", "amount": 49.99, "currency": "USD", "description": "Premium Subscription" }' ``` ## Get Payment by Invoice ID Retrieve payment details by Lightning Enable invoice ID. ```http GET /api/payments/{invoiceId} ``` ### Parameters | Parameter | Type | Description | |-----------|------|-------------| | `invoiceId` | string | Lightning Enable invoice ID | ### Response ```json { "invoiceId": "1042", "orderId": "ORDER-12345", "providerChargeId": "charge_abc123", "openNodeChargeId": "charge_abc123", "status": "paid", "amount": 49.99, "currency": "USD", "lightningInvoice": "lnbc1250000n1pnxyz...", "onchainAddress": "bc1qxy2...", "hostedCheckoutUrl": "https://checkout.opennode.com/abc123", "payUrl": "https://api.lightningenable.com/pay/YOUR_PAYMENT_TOKEN", "createdAt": "2026-07-03T12:00:00Z", "paidAt": "2026-07-03T12:05:00Z", "expiresAt": "2026-07-03T13:00:00Z" } ``` ### Example ```bash curl https://api.lightningenable.com/api/payments/1042 \ -H "X-API-Key: YOUR_API_KEY" ``` ## Public Payment Status (browser polling) ```http GET /api/payments/{invoiceId}/status ``` **No API key required.** Returns only `{ "status": "..." }`, so it is safe to call from client-side code — this is the endpoint your checkout page should poll instead of embedding your API key in the browser: ```json { "status": "paid" } ``` ```javascript // Browser-safe polling const { status } = await (await fetch( `https://api.lightningenable.com/api/payments/${invoiceId}/status` )).json(); ``` Fulfillment decisions belong on your server — use webhooks or the authenticated endpoints above for the authoritative record. ## Get Payment by Order ID Retrieve payment details by your order ID. ```http GET /api/payments/order/{orderId} ``` ### Parameters | Parameter | Type | Description | |-----------|------|-------------| | `orderId` | string | Your order identifier | ### Response Same as Get Payment by Invoice ID. ### Example ```bash curl https://api.lightningenable.com/api/payments/order/ORDER-12345 \ -H "X-API-Key: YOUR_API_KEY" ``` ## Sync Payment Status Force sync payment status from the payment provider. Useful if webhook was delayed. ```http POST /api/payments/{invoiceId}/sync ``` ### Parameters | Parameter | Type | Description | |-----------|------|-------------| | `invoiceId` | string | Lightning Enable invoice ID | ### Response Returns updated payment object with current status. ### Example ```bash curl -X POST https://api.lightningenable.com/api/payments/1042/sync \ -H "X-API-Key: YOUR_API_KEY" ``` ## Payment Statuses | Status | Description | |--------|-------------| | `unpaid` | Invoice created, awaiting payment | | `processing` | Payment detected, confirming | | `paid` | Payment confirmed | | `expired` | Invoice expired without payment | | `refunded` | Payment was refunded | ## Payment Flow 1. `POST /api/payments` returns `invoiceId`, `lightningInvoice` and `payUrl`. 2. Show the invoice as a QR code (or send the buyer to `payUrl`); the buyer pays from any Lightning wallet. 3. Your callback URL gets a [webhook](/api-reference/webhooks), or you read `GET /api/payments/{invoiceId}`. 4. When `status` is `paid`, fulfill the order. ## Supported Currencies | Currency | Code | Notes | |----------|------|-------| | US Dollar | `USD` | Converted to BTC at current rate | | Euro | `EUR` | Converted to BTC at current rate | | British Pound | `GBP` | Converted to BTC at current rate | | Bitcoin | `BTC` | Direct amount as a decimal with up to 8 places — for satoshi precision, use BTC (e.g. `0.00062500` = 62,500 sats) | Currency codes must be **uppercase 3-letter** values — there is no `sats` currency code; lowercase or 4-letter values fail validation with a 400. :::note Nostr Wallet Connect accounts invoice in BTC only If your account settles into your own wallet over [Nostr Wallet Connect](/nwc-setup/account-setup), send `BTC` (amount in bitcoin) or `SAT` (whole satoshis), between 1 satoshi and 21,000,000 BTC. A fiat code returns `400 currency_not_supported`, and an amount out of range returns `400 amount_too_small` or `amount_too_large`; see [NWC Request Refused](/api-reference/errors#nwc-request-refused-currency-or-amount). An omitted `currency` defaults to `USD`, so always set it. ::: On the Nostr Wallet Connect lane (your own wallet), only `BTC` is accepted: a Lightning wallet has no exchange rate to convert fiat with. ## Invoice Expiration Set the lifetime yourself with `expiresInSeconds` (60 to 86400). Without it, the lifetime is your payment provider's default. Either way, read `expiresAt` from the response instead of assuming a window: it's the deadline encoded in the BOLT11 invoice, which is the one a payer's wallet enforces. | Provider | Without `expiresInSeconds` | With `expiresInSeconds` | |----------|----------------------------|-------------------------| | **Strike** (recommended default) | About 24 hours | Sent as `bolt11.expiryInSeconds` on Strike's [create receive request](https://docs.strike.me/api/create-a-receive-request) | | **OpenNode** | 24 hours (OpenNode's `ttl` default) | Sent as `ttl` in whole minutes, rounded down. OpenNode's minimum is 10 minutes, so a value under 600 gets a 10-minute invoice; `expiresAt` shows the real deadline | | **Your own wallet (NWC)** | The wallet's default | Sent as `expiry` on the NIP-47 `make_invoice` request. A wallet that ignores it shows its own deadline in `expiresAt` | | **Demo invoices** (`amount: 0`) | 10 minutes | The value you send | If the provider refuses the request (for example, an expiry it won't accept), the response is `502 payment_provider_rejected` with the provider's reason in `message`. If the provider is down (it answered with a 5xx), the response is `502 payment_provider_unavailable`: retry later. Either way no invoice is created, and `retryable` says whether retrying the same request can succeed. When a price only holds for a while, mint the invoice to die with it. The Lightning Enable Store's custom-product checkout does this: it sets `expiresInSeconds` to the time left on the price quote, because an order is refused with `400 invoice_outlives_quote` when its invoice stays payable more than an hour (by default) past the quote's expiry. After expiration an invoice can't be paid — create a new payment. ## Error Responses Errors are JSON with an `error` field. Some are prose; dispatch those on the HTTP status: ```json // 400 — business validation (e.g. duplicate order) { "error": "Invoice already exists for OrderId ORDER-12345" } // 404 — unknown invoice, or another account's { "error": "Invoice 1042 not found for merchant 42" } ``` These create-payment errors carry a machine-readable `error` code and a `message`. The `502` bodies below are illustrative: `message` and `providerStatus` carry whatever the provider actually answered. ```json // 400 — expiresInSeconds outside 60..86400, or not a whole number { "error": "invalid_expiry", "message": "expiresInSeconds must be a whole number between 60 and 86400 seconds (1 minute to 24 hours), or omitted to use the payment provider's default invoice lifetime. Nothing was created.", "minExpiresInSeconds": 60, "maxExpiresInSeconds": 86400 } // 400 — no payment provider credential on your account (same body on POST /api/payments/{invoiceId}/sync) { "error": "payment_provider_not_configured", "message": "No payment provider API key is configured on this account, so no Lightning invoice could be created and nothing was charged. ...", "docs": "https://docs.lightningenable.com/strike-setup/account-setup" } // 503 — your own wallet (NWC lane) is busy; sent with Retry-After: 2 { "error": "payment_provider_busy", "message": "The Lightning wallet settling this payment is busy serving other requests and did not free a slot in time, so no invoice was created and nothing was charged. ...", "docs": "https://docs.lightningenable.com/nwc-setup/account-setup" } // 502 — the payment provider refused to create the invoice (illustrative) { "error": "payment_provider_rejected", "message": "The payment provider (strike) refused to create the invoice (HTTP 422): INVALID_DATA: Invalid data. (bolt11.expiryInSeconds: Value is out of range.) Nothing was created. Fix the request and retry; if it persists, contact support@lightningenable.com.", "provider": "strike", "providerStatus": 422, "retryable": false } // 502 — the payment provider is down (illustrative) { "error": "payment_provider_unavailable", "message": "The payment provider (strike) is unavailable (HTTP 503), so no invoice was created. Retry later; if it persists, contact support@lightningenable.com.", "provider": "strike", "providerStatus": 503, "retryable": true } ``` Model-binding failures (missing `orderId`, malformed `currency`, negative `amount`) return the standard ASP.NET validation problem shape — a 400 with an `errors` dictionary keyed by field name. See [Errors](/api-reference/errors) for the full contract. ## Code Examples ### JavaScript ```javascript async function createPayment(orderId, amount, currency) { const response = await fetch('https://api.lightningenable.com/api/payments', { method: 'POST', headers: { 'X-API-Key': process.env.LIGHTNING_ENABLE_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ orderId, amount, currency }) }); if (!response.ok) { const { error, message } = await response.json(); throw new Error(`Payment failed (${response.status} ${error}): ${message ?? ''}`); } return response.json(); } // Usage const payment = await createPayment('ORDER-123', 49.99, 'USD'); console.log('Lightning Invoice:', payment.lightningInvoice); ``` ### Python ```python import os import requests def create_payment(order_id, amount, currency): response = requests.post( 'https://api.lightningenable.com/api/payments', headers={ 'X-API-Key': os.environ['LIGHTNING_ENABLE_API_KEY'], 'Content-Type': 'application/json' }, json={ 'orderId': order_id, 'amount': amount, 'currency': currency } ) response.raise_for_status() return response.json() ``` ## Next Steps - [Webhooks](/api-reference/webhooks) - Real-time payment notifications - [Errors](/api-reference/errors) - Error handling ============================================================================== # Rate Limiting Source: https://docs.lightningenable.com/api-reference/rate-limiting ============================================================================== # Rate Limiting Lightning Enable rate-limits requests to keep the service stable. Every request passes the global limit, and most routes also pass one named policy. ## Limits ### Global limit **100 requests per minute**, fixed window, per API key (per IP address for requests without an `X-API-Key`). The store API routes (`/api/store-*`, `/api/community-submissions`) have their own bucket of 600 a minute, so store traffic never spends this budget. ### Named policies | Policy | Limit | Scope | Applies to | |--------|-------|-------|-----------| | `merchant-key` | 600 / min (sliding) | Per API key | `/api/merchant/*` (including `POST /api/merchant/webhook-secret/rotate`), `/api/l402/challenges*`, `/api/proxy/*` (proxy and manifest management) | | `payment-create` | 10 / min | Per API key (per IP on public routes) | `POST /api/payments`, `POST /api/refunds`, `/api/checkout/*` | | `read` | 200 / min | Per API key (per IP on public routes, such as `GET /api/payments/{invoiceId}/status`) | `GET /api/payments/*`, `POST /api/payments/{invoiceId}/sync`, `GET /api/refunds/*` | | `write` | 20 / min | Per API key | `POST /api/merchant/regenerate-key` | | `l402-endpoints` | 20 / min | Per API key | `GET /api/l402/endpoints` | | `l402-renewal` | 5 / min to get a challenge, 60 / min to redeem | Per API key | `POST /api/merchant/subscription/renew` | | `checkout` | 10 / min per IP, 60 / min in total | Per IP, plus a ceiling shared by all callers | `POST /api/bitcoin/create-subscription` | | `checkout-create` | 5 / min per IP, 60 / min in total | Per IP, plus a ceiling shared by all callers | `POST /api/stripe/create-checkout-session`, `POST /api/shopify/{slug}/checkout` | | `store` | 600 / min (sliding) | Per API key | Store API routes | | `webhook` | 100 / min (sliding) | Shared | `/api/webhooks/*` (payment provider callbacks) | | `admin` | 30 / min | Per API key (there is one admin key, so all operators share it) | `/api/admin/*` | | Signup and sign-in | 10 per 15 min (free signup, Fast Lane signup, magic link); 20 / min (dashboard API-key sign-in); 30 / min (Fast Lane quote) | Per IP | `/api/signup/*`, `/dashboard/auth/*` | Per-key and per-IP policies refuse at once, and one merchant using up its budget never slows down another. The checkout ceilings exist because each of those requests creates an invoice or checkout session at a payment provider. `webhook` is the one shared bucket: provider callbacks come from a few provider addresses and carry no merchant identifier, so it queues a few excess requests briefly instead of dropping a provider's burst. A `429` there doesn't always mean your own traffic was heavy. `/l402/proxy/*` has only the global limit. ## Rate limit exceeded A refused request gets `429 Too Many Requests` with a `Retry-After` header (whole seconds) and this body: ```http HTTP/1.1 429 Too Many Requests Retry-After: 12 Content-Type: application/json { "error": "Too many requests", "message": "Rate limit exceeded. Please try again later.", "retryAfter": 12 } ``` `retryAfter` in the body carries the same wait. A browser navigation gets a small HTML page instead of JSON, and a dashboard form gets redirected back to its page with an error. The API doesn't send `X-RateLimit-*` headers. ## Failed-authentication throttling Separately, Lightning Enable counts **failed authentications per IP address**. More than **20 failures in a 60-second window** blocks authenticated requests from that IP until the window ends: ```http HTTP/1.1 429 Too Many Requests Retry-After: 42 Content-Type: application/json { "error": "Too many failed authentication attempts", "message": "Slow down and try again in 42 seconds" } ``` | | Rate limit | Auth-failure throttle | |---|---|---| | Trigger | Too many requests | Too many missing or invalid API keys | | Scope | See the policy table | Per IP, whatever keys were tried | | `error` | `"Too many requests"` | `"Too many failed authentication attempts"` | | What to do | Wait `Retry-After`, then retry | **Fix your API key.** Retrying with the same key re-arms the block | Check your key at **Dashboard → Settings**. See [Authentication](/api-reference/authentication#failed-authentication-throttling). ## Handling 429s Wait for `Retry-After`, and stop on the auth throttle: ```javascript async function requestWithRetry(url, options, maxRetries = 3) { for (let attempt = 0; attempt < maxRetries; attempt++) { const response = await fetch(url, options); if (response.status !== 429) return response; const body = await response.json(); if (body.error === 'Too many failed authentication attempts') { throw new Error('Authentication throttled. Check your API key.'); } const seconds = Number(response.headers.get('Retry-After')) || body.retryAfter || 60; await new Promise(r => setTimeout(r, seconds * 1000)); } throw new Error('Max retries exceeded'); } ``` ```python import time import requests def request_with_retry(method, url, max_retries=3, **kwargs): for _ in range(max_retries): response = requests.request(method, url, **kwargs) if response.status_code != 429: return response body = response.json() if body.get('error') == 'Too many failed authentication attempts': raise RuntimeError('Authentication throttled. Check your API key.') time.sleep(int(response.headers.get('Retry-After', body.get('retryAfter', 60)))) raise RuntimeError('Max retries exceeded') ``` ## Stay under the limits - **Use webhooks instead of polling.** Your callback URL is told when a payment changes state; see [Webhooks](/api-reference/webhooks). For browser-side polling, use the public `GET /api/payments/{invoiceId}/status`. - **Send an idempotency key** on payment and challenge creation, so a retry after a `429` can't create a second invoice. See [Request Headers](/api-reference/headers). - **Cap your concurrency** when backfilling, for example 5 requests in flight at a time. Need higher limits? Email support@lightningenable.com. ## Next steps - [Errors](/api-reference/errors) - Error handling - [Authentication](/api-reference/authentication) - API key setup - [Webhooks](/api-reference/webhooks) - Avoid polling with webhooks ============================================================================== # Settlements API Source: https://docs.lightningenable.com/api-reference/settlements ============================================================================== # Settlements API :::info Settlements = Completed Payments In Lightning Enable, settlements are completed payments. The same API endpoints handle both. See the **[Payments API](./payments.md)** for the complete reference. ::: When a Lightning payment is confirmed, it becomes a settlement. Lightning Enable does not hold funds: your payment provider (Strike or OpenNode), or your own wallet over Nostr Wallet Connect, facilitates custody and settlement. ## Key Endpoints All payment endpoints in the [Payments API](./payments.md) apply to settlements: - `POST /api/payments` — Create a payment (which settles automatically on confirmation) - `GET /api/payments/{invoiceId}` — Check payment/settlement status - `POST /api/payments/{invoiceId}/sync` — Force sync status from your payment provider ## Settlement Webhooks You'll receive a webhook notification when a payment settles. There is no event envelope and no `payment.completed` event type — Lightning Enable forwards **one flat JSON object per payment event** and you route on its `status` field (treat `status: "paid"` as the settlement/fulfillment trigger). See [Webhooks](./webhooks.md) for the exact payload shapes and signature verification. ============================================================================== # Webhooks Source: https://docs.lightningenable.com/api-reference/webhooks ============================================================================== # Webhooks Receive real-time notifications when payment events occur. ## Overview Webhooks notify your application when: - A payment is completed - A payment expires or its status changes Instead of polling the API, webhooks push events to your server instantly. ## How It Works 1. The customer pays the Lightning invoice. 2. Your payment provider tells Lightning Enable (Strike and OpenNode send a webhook; on the Nostr Wallet Connect lane, Lightning Enable checks your wallet every minute). 3. Lightning Enable sends a signed webhook to your callback URL. ## Webhook Payload Lightning Enable forwards **one flat JSON object per event** to your callback URL. There is no envelope. Two families of event arrive on the same URL, with the same signature scheme, and they are told apart by which discriminator is present: | Family | Discriminator | Sent when | |---|---|---| | **Payment events** | `status` (no `event` field) | A provider invoice from `POST /api/payments` changes state | | **L402 producer events** | `event` (no `status` field) | A challenge you minted is proven paid — see [L402 producer events](#l402-producer-events) | Check for `event` first and fall through to `status`, so a new producer event never lands in your payment-status switch. The payment payload's shape depends on which payment provider your merchant account uses. **OpenNode merchants:** ```json { "invoiceId": "1042", "orderId": "ORDER-12345", "status": "paid", "amount": 49.99, "currency": "USD", "openNodeChargeId": "abc123-def456-...", "paidAt": "2026-07-03T12:05:00Z", "metadata": "{\"customerId\":\"cust_42\"}" } ``` **Strike merchants:** ```json { "invoiceId": "1042", "orderId": "ORDER-12345", "status": "paid", "amount": 49.99, "currency": "USD", "providerChargeId": "8f6c3f5e-1c2d-...", "provider": "strike", "paidAt": "2026-07-03T12:05:00Z", "metadata": null } ``` | Field | Description | |-------|-------------| | `invoiceId` | Lightning Enable invoice ID (numeric string) — the same `invoiceId` returned by `POST /api/payments` | | `orderId` | Your order ID from payment creation | | `status` | Payment status string. Treat `paid` as the fulfillment trigger. Other values include `processing`, `expired`, and (OpenNode) `underpaid` / `refunded` | | `amount` / `currency` | The invoice amount and currency you created it with | | `openNodeChargeId` / `providerChargeId` | The provider's charge ID. Strike and NWC payloads also carry `provider` (`"strike"` or `"nwc"`); on NWC, `providerChargeId` is the invoice's payment hash | | `paidAt` | When Lightning Enable processed the provider event (UTC) | | `metadata` | The metadata JSON string you supplied at creation, or `null` | **Nostr Wallet Connect merchants** get the Strike shape with `"provider": "nwc"`. ### L402 producer events If you sell through the [L402 producer API](/products/agentic-commerce/producer-api-reference), a second event type arrives on the same URL: ```json { "event": "l402.challenge.paid", "paymentHash": "abc123def456...", "resource": "/api/premium/weather", "amountSats": 100, "paidAt": "2026-09-05T18:00:41Z", "idempotencyKey": "req-abc-123" } ``` | Field | Description | |-------|-------------| | `event` | Always `l402.challenge.paid`. Its presence is what distinguishes this from a payment event | | `paymentHash` | Hex payment hash of the challenge's invoice — your correlation handle, and the key to dedupe on | | `resource` | The resource path the challenge was minted for | | `amountSats` | Price in satoshis the challenge required | | `paidAt` | When payment was first proven (UTC) | | `idempotencyKey` | The `Idempotency-Key` the challenge was minted under, or `null` | **Fires once per challenge**, on the first successful verification of a credential minted from it — through `/api/l402/challenges/verify`, `/verify-credential`, or an L402-gated proxy request. L402 tokens stay valid for repeated use until their `expires` caveat, so the same credential verifies many times per paid invoice; only the first transition notifies. **"Paid" means proven, not settled.** The payer can only hold a valid credential by having settled the invoice with your payment provider — but the sats moved when the invoice was paid, which may be moments before this event. Lightning Enable does not hold funds; your provider is the record of what you were paid. An invoice that was paid but whose credential is never presented back produces no event; reconcile those with `GET /api/l402/challenges?status=unpaid`. No callback URL configured means no event is sent and nothing is queued. ## Configuring Webhooks Set your callback URL and signing secret with [`PUT /api/merchant/webhook-url`](/api-reference/merchant-settings#update-webhook-url): ```bash curl -X PUT https://api.lightningenable.com/api/merchant/webhook-url \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "webhookUrl": "https://example.com/webhooks/lightning", "webhookSecret": "a-long-random-string" }' ``` **Always set a secret.** Without one, deliveries go out unsigned (no `X-LightningEnable-Signature` header), and a receiver that requires a signature rejects every one. Your endpoint must be publicly reachable over HTTPS and answer `2xx` within **10 seconds**. ### Get the signing secret Every account gets a signing secret at signup, but the merchant API doesn't return it. To verify deliveries, either send your own `webhookSecret` as above, or rotate the secret and store the value the response returns: ```bash curl -X POST https://api.lightningenable.com/api/merchant/webhook-secret/rotate \ -H "X-API-Key: YOUR_API_KEY" ``` ```json { "webhookSecret": "", "message": "Webhook secret rotated. Store it now: it is not readable again through the merchant API; rotate to get a new one. ..." } ``` The merchant API returns the secret only in this response; to get a value again, rotate again. `GET /api/merchant/me` reports only `onboarding.hasWebhookSecret`. If two rotate calls overlap, the response of the last successful call holds the live secret. **Rotation takes effect immediately, including for deliveries already queued.** Lightning Enable reads your current secret when it sends each delivery, not when it queues the event, so a retry of an earlier event is signed with the new secret. Deploy the new secret to your receiver as soon as you rotate. To rotate without dropping events, have your receiver accept either the old or the new secret for a few minutes. ## Verifying Webhooks Every webhook request from Lightning Enable includes an `X-LightningEnable-Signature` header. You **must** verify this signature to confirm that the webhook is authentic and has not been tampered with. ### Signature Format The signature header uses a timestamped HMAC scheme: ```http POST /webhooks/lightning HTTP/1.1 Content-Type: application/json X-LightningEnable-Signature: t=1704067200,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8f9 ``` The header contains two comma-separated components: | Component | Description | |-----------|-------------| | `t` | Unix timestamp (seconds) when the signature was generated | | `v1` | HMAC-SHA256 hex digest of the signed payload | ### Verification Steps To verify a webhook signature: 1. **Extract** the timestamp (`t`) and signature (`v1`) from the `X-LightningEnable-Signature` header 2. **Construct** the signed payload by concatenating the timestamp, a period (`.`), and the raw request body: `{timestamp}.{payload}` 3. **Compute** the HMAC-SHA256 of the signed payload using your webhook secret as the key 4. **Compare** the computed signature with the `v1` value using a constant-time comparison function to prevent timing attacks 5. **Check freshness** -- reject signatures where the timestamp is more than 5 minutes old to prevent replay attacks ### Replay Protection Reject a webhook whose `t` is more than 5 minutes old (and, optionally, more than 30 seconds in the future). Every delivery attempt is signed with a fresh `t`, so a retry still passes this check, while a captured request can't be replayed later. ### Node.js / JavaScript Verification ```javascript const crypto = require('crypto'); const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET; const TOLERANCE_SECONDS = 300; // 5 minutes /** * Parse the X-LightningEnable-Signature header into its components. */ function parseSignatureHeader(header) { const parts = header.split(','); let timestamp = null; let signature = null; for (const part of parts) { const trimmed = part.trim(); if (trimmed.startsWith('t=')) { timestamp = parseInt(trimmed.slice(2), 10); } else if (trimmed.startsWith('v1=')) { signature = trimmed.slice(3); } } return { timestamp, signature }; } /** * Verify a webhook signature with replay protection. * Returns true if the signature is valid and the timestamp is fresh. */ function verifyWebhookSignature(payload, signatureHeader, secret) { const { timestamp, signature } = parseSignatureHeader(signatureHeader); if (!timestamp || !signature) { return false; } // Check timestamp freshness (replay protection) const now = Math.floor(Date.now() / 1000); if (Math.abs(now - timestamp) > TOLERANCE_SECONDS) { return false; } // Compute expected signature over "{timestamp}.{payload}" const signedPayload = `${timestamp}.${payload}`; const expected = crypto .createHmac('sha256', secret) .update(signedPayload) .digest('hex'); // Constant-time comparison return crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(expected) ); } // Express.js example -- use express.raw() to get the raw body app.post('/webhooks/lightning', express.raw({ type: 'application/json' }), (req, res) => { const signatureHeader = req.headers['x-lightningenable-signature']; const payload = req.body.toString('utf8'); if (!signatureHeader || !verifyWebhookSignature(payload, signatureHeader, WEBHOOK_SECRET)) { return res.status(401).send('Invalid signature'); } const event = JSON.parse(payload); handleWebhookEvent(event); res.status(200).send('OK'); }); ``` :::caution Use the raw request body You must verify the signature against the **raw request body** string, not a re-serialized JSON object. Re-serialization may change whitespace or key ordering, causing signature mismatches. ::: ### C# Verification ```csharp using System.Security.Cryptography; using System.Text; using System.Text.Json; [ApiController] [Route("webhooks")] public class WebhookController : ControllerBase { private readonly string _webhookSecret; private const int ToleranceSeconds = 300; // 5 minutes public WebhookController(IConfiguration config) { _webhookSecret = config["WebhookSecret"]!; } [HttpPost("lightning")] public async Task HandleWebhook() { // Read the raw body using var reader = new StreamReader(Request.Body); var payload = await reader.ReadToEndAsync(); var signatureHeader = Request.Headers["X-LightningEnable-Signature"].FirstOrDefault(); if (string.IsNullOrEmpty(signatureHeader) || !VerifySignature(payload, signatureHeader)) { return Unauthorized("Invalid signature"); } var webhookEvent = JsonSerializer.Deserialize(payload); await ProcessEventAsync(webhookEvent); return Ok(); } private bool VerifySignature(string payload, string signatureHeader) { // Parse "t={timestamp},v1={signature}" if (!TryParseSignatureHeader(signatureHeader, out var timestamp, out var providedSignature)) { return false; } // Replay protection: reject if older than 5 minutes var signatureTime = DateTimeOffset.FromUnixTimeSeconds(timestamp); var age = DateTimeOffset.UtcNow - signatureTime; if (age.TotalSeconds > ToleranceSeconds || age.TotalSeconds < -30) { return false; } // Compute HMAC-SHA256 over "{timestamp}.{payload}" var signedPayload = $"{timestamp}.{payload}"; using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(_webhookSecret)); var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(signedPayload)); var expectedSignature = Convert.ToHexString(hash).ToLowerInvariant(); // Constant-time comparison return CryptographicOperations.FixedTimeEquals( Encoding.UTF8.GetBytes(expectedSignature), Encoding.UTF8.GetBytes(providedSignature)); } private static bool TryParseSignatureHeader(string header, out long timestamp, out string signature) { timestamp = 0; signature = string.Empty; foreach (var part in header.Split(',')) { var trimmed = part.Trim(); if (trimmed.StartsWith("t=") && long.TryParse(trimmed[2..], out timestamp)) { } else if (trimmed.StartsWith("v1=")) { signature = trimmed[3..].ToLowerInvariant(); } } return timestamp > 0 && !string.IsNullOrEmpty(signature); } } ``` ### Python Verification ```python import hmac import hashlib import time WEBHOOK_SECRET = os.environ['WEBHOOK_SECRET'] TOLERANCE_SECONDS = 300 # 5 minutes def parse_signature_header(header): """Parse the X-LightningEnable-Signature header.""" timestamp = None signature = None for part in header.split(','): trimmed = part.strip() if trimmed.startswith('t='): timestamp = int(trimmed[2:]) elif trimmed.startswith('v1='): signature = trimmed[3:] return timestamp, signature def verify_webhook_signature(payload, signature_header, secret): """Verify a webhook signature with replay protection.""" timestamp, signature = parse_signature_header(signature_header) if timestamp is None or signature is None: return False # Replay protection: reject if older than 5 minutes now = int(time.time()) if abs(now - timestamp) > TOLERANCE_SECONDS: return False # Compute HMAC-SHA256 over "{timestamp}.{payload}" signed_payload = f'{timestamp}.{payload}' expected = hmac.new( secret.encode('utf-8'), signed_payload.encode('utf-8'), hashlib.sha256 ).hexdigest() # Constant-time comparison return hmac.compare_digest(signature, expected) # Flask example @app.route('/webhooks/lightning', methods=['POST']) def handle_webhook(): signature_header = request.headers.get('X-LightningEnable-Signature') payload = request.get_data(as_text=True) if not signature_header or not verify_webhook_signature(payload, signature_header, WEBHOOK_SECRET): return 'Invalid signature', 401 event = request.json process_event(event) return 'OK', 200 ``` ## Handling Events Return `200` quickly and do slow work in the background. The same event can arrive more than once, so make your handler idempotent: dedupe payment events on `invoiceId` + `status`, and producer events on `paymentHash`. ```javascript async function handlePaid(payload) { const { invoiceId } = payload; // Check if already processed const existing = await db.orders.findOne({ lightningInvoiceId: invoiceId, status: 'fulfilled' }); if (existing) { console.log('Already processed:', invoiceId); return; } // Process the payment await db.orders.updateOne( { lightningInvoiceId: invoiceId }, { $set: { status: 'fulfilled', paidAt: payload.paidAt } } ); await fulfillOrder(invoiceId); } ``` ### Routing on `event`, then `status` Check for an `event` field first — that is an L402 producer event. Everything else is a payment event, dispatched on `status`: ```javascript app.post('/webhooks/lightning', async (req, res) => { const payload = req.body; // verify the signature first — see above if (payload.event === 'l402.challenge.paid') { await handleChallengePaid(payload); // dedupe on payload.paymentHash return res.status(200).send('OK'); } switch (payload.status) { case 'paid': await handlePaid(payload); break; case 'expired': await handleExpired(payload); break; default: // processing, underpaid, etc. — log and wait for a terminal status console.log('Payment update:', payload.invoiceId, payload.status); } res.status(200).send('OK'); }); ``` ## Delivery & Reliability Webhook delivery is a **fast-path notification, not a guaranteed delivery channel**: - Every delivery attempt, the first one and each retry, has a **10-second timeout**. The first attempt goes out as soon as Lightning Enable learns of the change. - If your endpoint is down or answers with an error, delivery is retried with exponential backoff (starting at 30 seconds and doubling), up to five attempts in all. Every attempt sends **identical payload bytes** with a freshly timestamped signature, so dedupe on the payload, never on the signature header. - After the last attempt the event is marked failed. Recover by polling, or email support@lightningenable.com to have failed events re-sent once your endpoint is fixed. *(As of November 2026, every delivery attempt, including each retry, times out at 10 seconds. Previously the retries waited up to 100 seconds, so a receiver that took longer than 10 seconds could still succeed on a retry. If your handler takes longer than that, acknowledge with a 2xx first and process the event asynchronously.)* **Recovery pattern:** on startup or on a schedule, reconcile any orders still pending on your side via `GET /api/payments/{invoiceId}` (authoritative status), or force a provider re-check with `POST /api/payments/{invoiceId}/sync`. ## Testing Webhooks ### Local Development Use ngrok to expose your local server: ```bash # Start your local server npm start # Runs on http://localhost:3000 # In another terminal ngrok http 3000 # Use the ngrok URL as your webhook endpoint # https://abc123.ngrok.io/webhooks/lightning ``` ### Test Event Send a test webhook to verify your endpoint: ```bash # Generate a test signature TIMESTAMP=$(date +%s) PAYLOAD='{"invoiceId":"1042","orderId":"ORDER-TEST","status":"paid","amount":25.00,"currency":"USD","openNodeChargeId":"test-charge","paidAt":"2026-07-03T12:05:00Z","metadata":null}' SIG=$(echo -n "${TIMESTAMP}.${PAYLOAD}" | openssl dgst -sha256 -hmac "your-webhook-secret" | cut -d' ' -f2) curl -X POST https://your-site.com/webhooks/lightning \ -H "Content-Type: application/json" \ -H "X-LightningEnable-Signature: t=${TIMESTAMP},v1=${SIG}" \ -d "${PAYLOAD}" ``` ## Troubleshooting ### Webhook Not Received 1. **Check the URL** you saved with `PUT /api/merchant/webhook-url`: `GET /api/merchant/me` shows `onboarding.hasWebhookUrl`. 2. **Check your firewall** allows inbound requests. 3. **Test manually** with the curl example above. 4. **Every delivery rejected with `401`?** You may have no signing secret saved, so deliveries arrive unsigned. Save one. If you never set or rotated the secret, you don't hold the value deliveries are signed with: call `POST /api/merchant/webhook-secret/rotate` and deploy the returned secret. Rotate once and store the response before you send another rotate call. ### Signature Mismatch 1. **Check secret** - Ensure webhook secret matches what you configured 2. **Check encoding** - Use the raw request body, not a re-serialized JSON object 3. **Check algorithm** - Use HMAC-SHA256 over `{timestamp}.{payload}` 4. **Check header name** - The header is `X-LightningEnable-Signature`, not `X-Webhook-Signature` 5. **Check timestamp** - Ensure replay protection tolerance is at least 5 minutes ### Timeout Errors Return `200` immediately and process in the background; a delivery times out after 10 seconds. Missed a webhook? `GET /api/payments/{invoiceId}` is authoritative. ## Security Best Practices - **Always verify the signature** against the raw body, as shown above. - **Use HTTPS** for your callback URL. - **Confirm before fulfilling high-value orders**: re-read the payment with `GET /api/payments/{invoiceId}` and fulfill only when `status` is `paid`. ## Next Steps - [Payments API](/api-reference/payments) - Create payments - [Errors](/api-reference/errors) - Error handling - [Rate Limiting](/api-reference/rate-limiting) - API limits ## Stuck? Start with [Troubleshooting](/troubleshooting) for the common failures and their fixes. If something's broken or not working, email support@lightningenable.com — a real person reads it. ============================================================================== # How Lightning Enable Works Source: https://docs.lightningenable.com/concepts ============================================================================== # How Lightning Enable Works Lightning Enable is API middleware between your application and a Lightning payment provider. You call one REST API to create invoices, sell API access over L402, and receive signed webhooks; Lightning Enable talks to the provider you configured. ``` Your app → Lightning Enable → Payment provider or your wallet → Lightning Network (API middleware) (custody and settlement) ``` ## What Lightning Enable does not do - **It does not hold funds.** Your payment provider (Strike or OpenNode), or your own wallet over Nostr Wallet Connect, facilitates custody and settlement. - **It does not control Bitcoin private keys** or sign transactions. - **It does not take a cut of payments.** Revenue is a flat subscription; there are no per-transaction fees. Your provider may charge its own fees. See [plans and pricing](/products/product-overview). - **It does not interpret payments.** Lightning Enable reports that an invoice settled. Whether that means an order, a subscription, or access to a resource is your application's decision. ## Invoice lifecycle Every invoice moves through these `status` values, as returned by the [Payments API](/api-reference/payments#payment-statuses) and forwarded in [webhooks](/api-reference/webhooks): | Status | Meaning | Final | |--------|---------|-------| | `unpaid` | Invoice created, awaiting payment | No | | `processing` | Payment detected, confirming | No | | `paid` | Payment confirmed | Yes | | `expired` | Invoice expired without payment | Yes | | `refunded` | Payment was refunded | Yes | Invoices are cheap and short-lived: create one when you need it and let unused ones expire. You can set the lifetime with `expiresInSeconds` on `POST /api/payments`. A paid Lightning invoice comes with a **preimage**, a 32-byte value whose SHA-256 hash equals the invoice's payment hash. Holding the preimage proves the invoice was paid; L402 uses it as the proof of payment. ## The three settlement lanes Each merchant chooses a lane. Lightning Enable uses the same API and the same webhook format on all three. | Lane | Who settles | How Lightning Enable learns of a payment | Notes | |------|-------------|-------------------------------------------|-------| | [Strike](/strike-setup/account-setup) (default) | Your Strike account | Strike webhook | Returns preimages, so it supports L402. Refunds supported. | | [Nostr Wallet Connect](/nwc-setup/account-setup) | Your own Lightning wallet | Polling: Lightning Enable checks open invoices every minute | Receive-only: no refunds, BTC/sats amounts only. | | [OpenNode](/opennode-setup/account-setup) (legacy) | Your OpenNode account | OpenNode webhook | Does not return preimages, so it can't complete L402 flows. | On every lane the flow is the same: 1. Your app calls `POST /api/payments`. Lightning Enable creates the invoice on your lane and returns it with an `invoiceId` and a hosted `payUrl`. 2. The customer pays the invoice from any Lightning wallet. 3. Lightning Enable detects the payment (webhook or poll) and updates the status. 4. Lightning Enable sends a signed webhook to your callback URL. Verify the `X-LightningEnable-Signature` header as described in [Verifying webhooks](/api-reference/webhooks#verifying-webhooks). You can also check status at any time with `GET /api/payments/{invoiceId}`. ## L402 in one paragraph L402 puts the payment inside the HTTP request. A client requests a paid resource and gets `402 Payment Required` with a Lightning invoice and a macaroon. It pays the invoice, receives the preimage, and retries with `Authorization: L402 :`. The server checks that `SHA256(preimage)` equals the invoice's payment hash and serves the response. No accounts or API keys are needed on the buyer's side, which is what lets AI agents pay for APIs on their own. Lightning Enable can issue and verify these challenges for you through a hosted [proxy](/products/agentic-commerce/proxy-setup-walkthrough), [middleware for your own API](/products/agentic-commerce/native-integration), or the [Producer API](/products/agentic-commerce/l402-producer-api). See [How L402 works](/products/agentic-commerce/how-it-works) for the full protocol. ## Next steps - [Create your first invoice](/getting-started/first-payment) - [Sell API access over L402](/products/agentic-commerce/overview) - [Payments API reference](/api-reference/payments) ============================================================================== # AI Spending Guidelines Source: https://docs.lightningenable.com/configuration/ai-spending-guidelines ============================================================================== # AI Spending Guidelines Suggested limits for an AI agent that pays over Lightning with the Lightning Enable MCP server. For how the controls work and why they hold, see [AI Spending Security](/products/agentic-commerce/ai-spending-security). ## How budgets are configured 1. **Operator limits** live in `~/.lightning-enable/config.json`: `limits.maxPerPayment` / `limits.maxPerSession` in USD, or `limits.maxPerPaymentSats` / `limits.maxPerSessionSats` in sats (the stricter wins when both are set). Only you can raise them, by editing the file and restarting the server. 2. **Runtime caps** can be lowered, never raised, by the agent with `budget(action="tighten", perRequest=..., perSession=...)` (`per_request` / `per_session` in Python). 3. **Confirmation**: payments above `tiers.autoApprove` / `tiers.logAndApprove` need a code the server prints to its console, which you give to the agent. See [AI Spending Security](/products/agentic-commerce/ai-spending-security#3-approve-larger-payments-out-of-band). ## Quick reference | Use case | Per payment | Per session | Wallet balance | |----------|-------------|-------------|----------------| | Testing and development | 100 sats | 1,000 sats | 5,000 sats | | Light browsing | 50 sats | 500 sats | 2,000 sats | | Research sessions | 200 sats | 2,000 sats | 10,000 sats | | API integration | 500 sats | 5,000 sats | 20,000 sats | | Heavy usage | 1,000 sats | 10,000 sats | 50,000 sats | To apply a row, set the sats limits in `config.json`. For example, the testing row: ```json { "limits": { "maxPerPaymentSats": 100, "maxPerSessionSats": 1000 } } ``` Sats limits need no BTC price feed, so they keep working when the price feed is down. Keep the wallet balance near the last column: it is the ceiling no software setting can raise. ## Typical costs | Operation | Typical cost | |-----------|--------------| | Single API call | 1-100 sats | | Premium content access | 10-500 sats | | AI model API (per request) | 50-200 sats | | Data feed query | 10-100 sats | | Image generation | 100-1,000 sats | | **Lightning Enable Store purchase** | **25,000-45,000+ sats** | :::warning Store purchases need higher limits [Lightning Enable Store](https://store.lightningenable.com) products cost **25,000-45,000+ sats** including shipping, which the default limits refuse. Raise the limits in `~/.lightning-enable/config.json` for that session: ```json { "limits": { "maxPerPayment": 50.00, "maxPerSession": 100.00 } } ``` Tools that take `maxSats` / `max_sats` (`access_l402_resource`, `pay_l402_challenge`, and the Python `pay_invoice`) default to 1,000 sats per call, so also pass a larger value, such as `maxSats=50000`. ::: ## Running a session **Before:** check the balance (`get_balance`) and the limits (`budget(action="status")`), and decide what the agent should be doing. **During:** watch for payments you didn't expect. To narrow scope, have the agent lower its caps with `budget(action="tighten")`. If a limit is hit, only you can raise it. **After:** review what was spent with `receipts(source="durable")` and compare it with your wallet's history. ## Budget strategy - **Start low.** Begin with the testing row, and raise limits only when legitimate use keeps hitting them and you understand why. - **Per-payment limits** stop a single large or accidental payment. **Per-session limits** stop many small payments, long sessions, and runaway loops from adding up. - **Don't raise a limit** just because it was hit, without reviewing the payments, or to avoid supervising the agent. - **Refill in small amounts.** Keep only near-term spending in the wallet, and never more than you are willing to lose. ## Troubleshooting ### "Exceeds per-request limit" The payment is above the effective per-payment cap: `maxPerPayment` (or `maxPerPaymentSats`) in the config file, a lower runtime cap from `budget(action="tighten")`, or the call's own `maxSats`. If the payment is legitimate, raise the config value and pass a larger `maxSats`; otherwise skip it or find a cheaper service. ### "Would exceed session budget" The session has spent most of its budget. Restart the MCP server to start a new session (the agent can't reset the counter), or raise `maxPerSession` in the config file. ### Wallet balance low Add funds through your wallet or payment provider, or lower your limits to match what is available. ## Related documentation - [AI Spending Security](/products/agentic-commerce/ai-spending-security) - security model and safeguards - [MCP Complete Guide](/products/agentic-commerce/mcp-complete-guide#budget-configuration) - every budget config key - [Legal Considerations](/configuration/legal-considerations) - liability and terms ============================================================================== # API Key Management Source: https://docs.lightningenable.com/configuration/api-key-management ============================================================================== # API Key Management Your merchant API key authenticates your requests to the Lightning Enable API. This page covers where to find it, how to rotate it, and how to keep it safe. ## Understanding API Keys Send it in the `X-API-Key` header on every authenticated request. It identifies your merchant account, so anyone holding it can act as you. ### API Key vs Provider API Key You have **two different keys** - don't confuse them: | Key | Purpose | Source | Used By | |-----|---------|--------|---------| | **Lightning Enable API Key** | Authenticate to Lightning Enable | Generated at signup | Your application → Lightning Enable | | **Provider credential (Strike key, OpenNode key, or NWC connection string)** | Connect Lightning Enable to your payment provider or wallet | Your provider dashboard or wallet app | Lightning Enable → your provider | ``` Your App Lightning Enable Strike or OpenNode │ │ │ │ X-API-Key: abc123... │ │ ├─────────────────────────────►│ │ │ (Lightning Enable API Key) │ │ │ │ Authorization: xyz789... │ │ ├─────────────────────────►│ │ │ (Provider API Key) │ ``` ## Getting Your API Key ### At Signup You receive your key once, when your account is created: - **Free Producer Sandbox:** after you open the confirmation link in your signup email, type the email address you signed up with on the page it opens, and press **Confirm and create my account**. Opening the link alone doesn't create the account. - **L402 Fast Lane:** in the response to the paid `POST /api/signup/l402` call (the MCP `create_lightning_enable_account` tool saves it for you). - **Stripe checkout:** on the success page after payment. :::warning Save Your Key Immediately The key is not emailed to you. Save it in a password manager or secret manager before you navigate away. If you lose it, reveal or regenerate it in the dashboard. ::: ### Key Management Dashboard Access your key anytime at: **https://api.lightningenable.com/dashboard/settings** The dashboard allows you to: - View your key (masked by default) and reveal or copy it - Regenerate it ## Using Your API Key ### In API Requests Include your API key in the `X-API-Key` header: ```bash curl -X POST https://api.lightningenable.com/api/payments \ -H "Content-Type: application/json" \ -H "X-API-Key: YOUR_API_KEY_HERE" \ -d '{ "orderId": "ORDER-1001", "amount": 10.00, "currency": "USD", "description": "Product purchase" }' ``` ### In Application Code #### .NET / C# ```csharp var client = new HttpClient(); client.DefaultRequestHeaders.Add("X-API-Key", Environment.GetEnvironmentVariable("LIGHTNING_ENABLE_API_KEY")); var response = await client.PostAsync("https://api.lightningenable.com/api/payments", content); ``` #### Node.js / JavaScript ```javascript const response = await fetch('https://api.lightningenable.com/api/payments', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-API-Key': process.env.LIGHTNING_ENABLE_API_KEY }, body: JSON.stringify({ orderId: 'ORDER-1001', amount: 10.00, currency: 'USD' }) }); ``` #### Python ```python import os import requests response = requests.post( 'https://api.lightningenable.com/api/payments', headers={ 'Content-Type': 'application/json', 'X-API-Key': os.environ['LIGHTNING_ENABLE_API_KEY'] }, json={'orderId': 'ORDER-1001', 'amount': 10.00, 'currency': 'USD'} ) ``` ## Managing Your API Key ### Viewing Your Key 1. Sign in at **https://api.lightningenable.com/dashboard/settings** (with your API key or a magic link). 2. In the **API Key** card, reveal or copy the key. ### Regenerating Your Key If your key is compromised or you want to rotate it for security: 1. Go to **https://api.lightningenable.com/dashboard/settings** 2. In the **API Key** card, click **"Regenerate API key"** 3. Confirm the action in the dialog 4. **Copy your new key immediately** 5. Update all your applications with the new key You can also regenerate with `POST /api/merchant/regenerate-key`; the response carries the new key once. :::danger Key Regeneration is Immediate and Irreversible - Your **old key is invalidated instantly** - Any application using the old key will receive `401 Unauthorized` - You **cannot recover the old key** - Each account has one key, so there is no overlap window. Have the new value ready to deploy before you regenerate. ::: ## Security Best Practices ### Do's | Practice | Why | |----------|-----| | Store in environment variables | Keeps keys out of code | | Use a secret manager | Centralized, audited secret storage | | Rotate keys periodically | Limits exposure if compromised | | Monitor for unauthorized use | Detect compromises early | ### Don'ts | Anti-Pattern | Risk | |--------------|------| | Hardcode in source code | Keys in git history forever | | Commit to version control | Public exposure | | Share via email/chat | Keys in searchable logs | | Log API keys | Exposure in log aggregators | ### Environment Variable Examples **Linux/macOS:** ```bash export LIGHTNING_ENABLE_API_KEY="YOUR_API_KEY_HERE" ``` **Windows PowerShell:** ```powershell $env:LIGHTNING_ENABLE_API_KEY = "YOUR_API_KEY_HERE" ``` **.env file (local development only):** ``` LIGHTNING_ENABLE_API_KEY=YOUR_API_KEY_HERE ``` :::warning Never Commit .env Files Add `.env` to your `.gitignore` file. ::: ### Secret Managers In production, keep the key in a secret manager (Azure Key Vault, AWS Secrets Manager, Google Cloud Secret Manager, HashiCorp Vault, or Kubernetes Secrets) and inject it at runtime. ## Troubleshooting ### "API key required" ```json {"error": "API key required", "message": "Please provide API key in X-API-Key header"} ``` **Cause:** Missing `X-API-Key` header **Fix:** Add the header to your request: ```bash -H "X-API-Key: YOUR_KEY_HERE" ``` ### "Invalid API key" (401) ```json {"error": "Invalid API key", "message": "The provided API key is invalid or inactive"} ``` **Causes:** the key was regenerated, mistyped, or the account is deactivated. **Fix:** use the current key from the dashboard, or regenerate it. ### "Subscription not active" (403) ```json {"error": "Subscription not active", "message": "...", "subscription_status": "past_due", "action_required": "update_payment_method"} ``` **Cause:** your key is valid, but your paid plan's Stripe subscription is `past_due`, canceled, or otherwise not active. Related 403s are `Subscription required` and `Subscription period expired`. Route on `action_required` (`subscribe`, `update_payment_method`, or `renew_subscription`). See [Errors](/api-reference/errors). ### Too many failed attempts (429) Repeated failed authentications from one IP are throttled. Wait for the `Retry-After` seconds, then retry with the correct key. ## Related Documentation - [Getting Started](/getting-started/quick-start) - Initial setup guide - [API Reference](/api-reference/authentication) - Authentication details - [Strike API Keys](/strike-setup/api-keys) - Configuring your payment provider key - [Webhooks](/api-reference/webhooks) - Securing webhook endpoints ## FAQ ### Can I have multiple API keys? No. Each merchant account has one API key. For a separate test environment, use a separate account; a [Free Producer Sandbox](/getting-started/activate-with-lightning) account costs nothing. ### How long is an API key valid? API keys don't expire. A key works until you regenerate it or the account is deactivated. A lapsed subscription does not invalidate the key, but paid features answer `403` until you renew. ### Can I see my old API key? No. When you regenerate, the old key is permanently invalidated and cannot be retrieved. This is a security feature. ### What if I lose my API key? Sign in to the dashboard with a magic link, then reveal or regenerate the key. ### Is the API key transmitted securely? Yes. The API is HTTPS-only, and the key is stored encrypted at rest. ============================================================================== # Environment Variables Source: https://docs.lightningenable.com/configuration/environment-variables ============================================================================== # Environment Variables Lightning Enable is hosted at `api.lightningenable.com`; you never set its server configuration. You configure your account in the [dashboard](https://api.lightningenable.com/dashboard) and with the [merchant API](/api-reference/merchant-settings). The environment variables on this page configure the open-source (MIT) **MCP server**, which you run wherever your agent runs. Platform server settings are an operator concern and are documented in the API repository (`appsettings.Example.json`), not here. ## MCP server configuration ### Wallet Configuration Wallet, invoice, L402, budget, and API-discovery tools work with just a wallet. Selling (`l402_producer`) and some `agent_services` actions also need `LIGHTNING_ENABLE_API_KEY`; see the table below. Choose one wallet (listed in default priority order). You can also store wallet credentials in `~/.lightning-enable/config.json` (the `setup_wallet` tool writes them there); an environment variable wins over the file. **LND (Best for L402):** ```bash LND_REST_HOST=https://your-lnd-node:8080 LND_MACAROON_HEX=your-admin-macaroon-hex ``` **Nostr Wallet Connect (NWC):** ```bash NWC_CONNECTION_STRING=nostr+walletconnect://pubkey?relay=wss://relay.example.com&secret=xxx ``` **Strike (Recommended for USD users):** ```bash STRIKE_API_KEY=your-strike-api-key ``` **OpenNode:** ```bash OPENNODE_API_KEY=your-opennode-api-key OPENNODE_ENVIRONMENT=production # or "dev" for testnet ``` :::info Wallet Priority If multiple wallet credentials are configured, they are used in this order: 1. LND (if `LND_REST_HOST` and `LND_MACAROON_HEX` are set) 2. NWC (if `NWC_CONNECTION_STRING` is set) 3. Strike (if `STRIKE_API_KEY` is set) 4. OpenNode (if `OPENNODE_API_KEY` is set) This order prioritizes wallets that return a preimage, which is required for L402. OpenNode does **not** return preimages, so L402 will not work with it. You can override the priority with the `WALLET_PRIORITY` environment variable (values: `lnd`, `nwc`, `strike`, `opennode`). Only the first configured wallet is used. ::: ### Spending Limits Configuration Budget limits are configured via `~/.lightning-enable/config.json`: ```json { "currency": "USD", "tiers": { "autoApprove": 0.10, "logAndApprove": 1.00, "formConfirm": 10.00, "urlConfirm": 100.00 }, "limits": { "maxPerPayment": 500.00, "maxPerSession": 100.00 } } ``` This file is created automatically on first run (or written by the `setup_wallet` tool). AI agents **cannot modify** this file through any MCP tool (an agent with direct shell or filesystem access to the host is outside this guarantee). At runtime, an agent can *tighten* its own caps via the `budget` tool's `action="tighten"` (formerly the standalone `configure_budget` tool), but it can never raise them above the limits in this file. See [AI Spending Security](/products/agentic-commerce/ai-spending-security) for detailed configuration. ### MCP Configuration Summary | Variable | Required | Default | Description | |----------|----------|---------|-------------| | `LND_REST_HOST` | If using LND | - | LND REST API host | | `LND_MACAROON_HEX` | If using LND | - | LND admin macaroon in hex | | `LND_TLS_CERT_PATH` | No | - | Path to the node's `tls.cert`; pins TLS to that certificate (use for self-signed certs) | | `LND_SKIP_TLS_VERIFY` | No | `false` | `true` disables TLS verification for the LND host. Prefer `LND_TLS_CERT_PATH` | | `NWC_CONNECTION_STRING` | If using NWC | - | Nostr Wallet Connect URI | | `STRIKE_API_KEY` | If using Strike | - | Strike API key (preferred for USD) | | `OPENNODE_API_KEY` | If using OpenNode | - | OpenNode API key with withdrawal permissions | | `OPENNODE_ENVIRONMENT` | No | `production` | `production` or `dev` | | `WALLET_PRIORITY` | No | - | Override default wallet priority (`lnd`, `nwc`, `strike`, or `opennode`) | | `LIGHTNING_ENABLE_API_KEY` | No | - | Merchant API key; required by every `l402_producer` action and the `agent_services` actions `request`/`publish`/`unpublish`/`attest` (`discover`/`settle`/`reputation` work without it) | | `LIGHTNING_ENABLE_TOOL_PROFILE` | No | `standard` | `lite` (pay and check the wallet only), `standard` (the full current surface), or `full` (standard plus every pre-consolidation tool name as a deprecated alias, removed in v3.0.0) — see the [MCP Complete Guide](/products/agentic-commerce/mcp-complete-guide#tool-profiles) | | `LIGHTNING_ENABLE_HOSTED` | No | unset | Set to `1` in a non-interactive/hosted deployment so out-of-band confirmation defaults to refusing (rather than printing to stderr) when the process isn't attached to a TTY — see [AI Spending Security](/products/agentic-commerce/ai-spending-security#confirmation-channels) | :::note Merchant payment provider keys are NOT environment variables The credential Lightning Enable uses to **receive** payments for you (Strike key, OpenNode key, or NWC connection string) is saved in the [dashboard](https://api.lightningenable.com/dashboard) (**Settings → Payment Provider**) or with the merchant API (`PUT /api/merchant/strike-key`, `/opennode-key`, `/nwc-connection`), never as an environment variable. The wallet variables above configure the **MCP server's paying wallet** only. ::: ## Next Steps - [MCP Complete Guide](/products/agentic-commerce/mcp-complete-guide) - Every tool and option - [Strike Setup](/strike-setup/account-setup) - Configure the default payment provider - [Quick Start](/getting-started/quick-start) - Test your configuration ============================================================================== # Legal Considerations Source: https://docs.lightningenable.com/configuration/legal-considerations ============================================================================== # Legal Considerations This document outlines the legal considerations, liability boundaries, and terms acknowledgment for using Lightning Enable's AI spending capabilities. ## Architecture Lightning Enable is **API middleware software**. The hosted API connects your platform to a payment provider (Strike or OpenNode) or to your own wallet over Nostr Wallet Connect. The open-source MCP server connects your AI agent to a wallet you configure (LND, NWC, Strike, or OpenNode). ### What Lightning Enable Does - Provides MCP tools that interact with your wallet - Forwards payment requests to your configured wallet provider - Enforces budget limits you configure - Tracks payment history for transparency ### What Lightning Enable Does NOT Do - Hold, custody, or control any Bitcoin (your payment provider or wallet facilitates custody and settlement) - Generate, store, or access Bitcoin private keys - Process payments (your payment provider or wallet does this) - Make spending decisions on your behalf - Guarantee payment success or reversal ### How It Works Lightning Enable forwards requests to the payment provider or wallet you configure. That provider or wallet facilitates custody and settlement, and you are the account holder. Consult legal counsel about your own regulatory obligations. ## Liability Boundaries ### User Responsibilities By using the `pay_invoice` tool, you acknowledge responsibility for: 1. **Authorizing AI Spending** - You are explicitly authorizing an AI agent to initiate payments - You understand the AI may make payments based on its interpretation of your requests - You accept that AI systems can make mistakes 2. **Configuration** - Setting appropriate budget limits - Using a dedicated wallet with limited funds - Securing your API keys - Monitoring spending activity 3. **Oversight** - Reviewing payments made by AI agents - Detecting and reporting unauthorized activity - Maintaining adequate supervision 4. **Funds** - All funds in your wallet are at risk - You accept potential loss up to your wallet balance - You will not hold Lightning Enable liable for spent funds ### Lightning Enable Does NOT Accept Liability For - Payments made by AI agents, intended or unintended - Misinterpretation of user intent by AI systems - Prompt injection or social engineering attacks - API key compromise or unauthorized use - Budget bypass exploits or software bugs - Third-party service failures (Strike, OpenNode, your wallet, Nostr relays, the Lightning Network) - Loss of funds due to any cause - Consequential damages from AI spending activity ### Risk Acknowledgment Using `pay_invoice` involves inherent risks: | Risk | Description | |------|-------------| | AI Misinterpretation | AI may pay invoices you didn't intend | | Prompt Injection | Malicious content may trigger payments | | Key Compromise | Stolen API keys enable unauthorized spending | | Software Bugs | Edge cases may bypass budget limits | | Network Issues | Payments may fail or be delayed | | Irreversibility | Lightning payments cannot be reversed | ## Terms of Use ### Explicit Acknowledgment By configuring and using the `pay_invoice` tool, you explicitly acknowledge: 1. **Voluntary Authorization** - You are voluntarily authorizing automated spending - You can revoke this at any time by removing the wallet credentials from the MCP server - Continued use constitutes continued authorization 2. **Assumption of Risk** - You understand and accept all risks described in this document - You are using AI spending capabilities at your own risk - You have been warned and proceed with informed consent 3. **Budget Limits** - Budget limits are advisory safeguards, not guarantees - Edge cases, race conditions, or bugs may allow spending beyond limits - You accept this possibility and have limited your wallet balance accordingly 4. **No Recourse** - You waive claims against Lightning Enable for spent funds - You will not seek recovery from Lightning Enable for any loss - Your recourse is limited to your relationship with your payment provider or wallet operator 5. **Age and Capacity** - You are of legal age to enter contracts in your jurisdiction - You have legal capacity to make financial decisions - You are not using this service where prohibited ### Jurisdictional Considerations - Bitcoin and Lightning Network may be regulated in your jurisdiction - AI spending may have tax implications - You are responsible for compliance with local laws - Lightning Enable makes no representations about legality in any jurisdiction ## Recommendations ### For Individual Users 1. **Use only discretionary funds** you can afford to lose completely 2. **Start with minimal amounts** (< $10 equivalent) 3. **Increase slowly** as you gain experience 4. **Never use** funds needed for bills, savings, or obligations 5. **Consult** a financial advisor if unsure ### For Businesses 1. **Create separate accounts** for AI spending 2. **Implement internal controls** and spending approval workflows 3. **Document** all AI spending authorization 4. **Audit** AI spending regularly 5. **Consult legal counsel** about liability exposure 6. **Consider insurance** for potential losses ### For Developers 1. **Test with small real payments** before raising limits 2. **Use minimal budgets** in production 3. **Log all payment attempts** for debugging 4. **Handle failures gracefully** 5. **Implement circuit breakers** for anomalous spending ## Indemnification You agree to indemnify, defend, and hold harmless Lightning Enable, its officers, directors, employees, and agents from any claims, damages, losses, or expenses (including attorney fees) arising from: - Your use of AI spending tools - Payments made through your configured wallet - Violation of these terms - Unauthorized use of your API keys - Your negligence or willful misconduct ## Contact For questions about these legal considerations: - **General inquiries:** support@lightningenable.com - **Legal matters:** legal@lightningenable.com - **Security issues:** security@lightningenable.com ## Related Documentation - [AI Spending Security](/products/agentic-commerce/ai-spending-security) - Security best practices - [Spending Guidelines](/configuration/ai-spending-guidelines) - Budget recommendations - [Terms of Service](/legal/terms-of-service) - [Privacy Policy](/legal/privacy-policy) ============================================================================== # FAQ Source: https://docs.lightningenable.com/faq ============================================================================== # Frequently Asked Questions ## General ### What is Lightning Enable? Lightning Enable is API middleware that connects your platform to a payment provider (Strike or OpenNode, or your own wallet over NWC) for Bitcoin Lightning payments. Lightning Enable does not hold funds; the payment provider facilitates custody and settlement. ### How is Lightning Enable different from a payment gateway? | Aspect | Traditional Gateway | Lightning Enable | |--------|---------------------|------------------| | Role | Processes payments | API middleware | | Fund custody | Gateway holds funds | Lightning Enable does not hold funds; the provider facilitates custody | | KYB/Compliance | Gateway handles | Provider handles | | Settlement | Gateway settles | Provider settles | | Revenue model | Per-transaction fees | Subscription only | ### Why do I need a payment provider account? Lightning Enable connects to a payment provider (Strike or OpenNode) that facilitates: - Bitcoin custody and settlement - KYB/KYC/AML compliance - Bank settlements (if desired) - Lightning Network infrastructure Lightning Enable connects to your provider account to create invoices and receive payment notifications. Strike is the default and recommended for most merchants. ## Pricing ### How much does Lightning Enable cost? | Plan | Price | Includes | |------|-------|----------| | Free Producer Sandbox | Free — no card | 3 endpoints, 200 challenges/month, 1,000 sats max per challenge | | Agentic Commerce | $49/month | Full REST API + L402 protocol + settlement via Strike API | | Agentic Commerce — Business | Contact us | Everything in Agentic Commerce + Strike or OpenNode + white-glove onboarding | Agentic Commerce and Agentic Commerce — Business both include webhook notifications, multi-currency pricing, priority email support, and access to all platform integrations (Shopify Commerce). Agentic Commerce also includes a **30-day free trial** through self-serve checkout; Business is contact-only (email support@lightningenable.com — it isn't purchasable through `/Checkout`), and any trial terms are arranged directly. ### Are there transaction fees? Lightning Enable charges a flat subscription fee only — no per-transaction fees from us. Your payment provider may charge their own processing fees, which you pay directly to them. ### Can I switch plans? Yes. Move from the Free Producer Sandbox to Agentic Commerce through checkout; for any other plan change, email support@lightningenable.com. ## Technical ### What programming languages are supported? Lightning Enable provides a REST API that works with any language. The docs include examples in JavaScript/TypeScript, C# / .NET, and Python. ### Do you have an SDK? Yes. L402 HTTP client libraries are available for auto-paying L402-protected APIs: - **Python:** [`l402-requests`](/tools/l402-requests) — three lines of code, paid APIs just work - **.NET:** [`L402Requests`](/tools/l402-dotnet) — same experience for .NET - **TypeScript/npm:** [`l402-requests`](/tools/l402-ts) — same experience for TypeScript See the [Developer Tools](/tools/l402-requests) section for installation and usage guides. For the Lightning Enable REST API itself, use your preferred HTTP client. ### Is there a sandbox environment? Not on the hosted platform. `api.lightningenable.com` connects to your provider's **production** API, so Strike sandbox and OpenNode testnet keys don't work. To test, create a demo invoice (`amount: 0`, auto-confirms without payment) or pay a small real invoice (about $1). See [Testing Your Strike Integration](/strike-setup/testing). ## Payments ### How long do invoices last? It depends on the provider: read `expiresAt` on the response. Strike invoices live about 24 hours by default; demo invoices (`amount: 0`) expire in 10 minutes. Send `expiresInSeconds` (60 to 86400) on `POST /api/payments` to set it yourself. An expired invoice can't be paid; create a new one. See [Invoice Expiration](/api-reference/payments#invoice-expiration). ### What currencies are supported? - **USD** - US Dollar - **EUR** - Euro - **GBP** - British Pound - **BTC** - Bitcoin (decimal, up to 8 places — use BTC for satoshi precision) Currency codes are uppercase 3-letter values; fiat currencies are converted to Bitcoin at current rates. NWC wallets accept BTC only. ### What happens if a payment fails? Lightning payments either succeed or fail; a failed payment moves no funds. The customer can retry the same invoice until it expires, then you create a new one. ## Webhooks ### How do I know when a payment is complete? Two options: 1. **Webhooks (recommended)** - Receive instant notifications 2. **Polling** - Check payment status via API Webhooks are more efficient and provide real-time updates. ### Are webhooks retried? Yes — failed deliveries are retried with exponential backoff (30s, 60s, 120s, 240s, 480s — 5 retry attempts over ~16 minutes), with identical payload bytes on every attempt (signatures are freshly timestamped per attempt — dedupe on payload content, never on the signature header). After retries exhaust, the event is marked permanently failed — recover by polling `GET /api/payments/{invoiceId}` or forcing a provider re-check with `POST /api/payments/{invoiceId}/sync`. ### How do I verify webhooks? Webhooks include an HMAC-SHA256 signature in the `X-LightningEnable-Signature` header in the format `t={timestamp},v1={hmac}`. Verify by computing HMAC-SHA256 of `{timestamp}.{payload}` using your webhook secret, and reject signatures older than 5 minutes. See the [Webhook Verification guide](/api-reference/webhooks#verifying-webhooks) for code examples. ## L402 Protocol ### What is L402? L402 is a protocol for HTTP 402 (Payment Required) that enables pay-per-request API access using Lightning payments. Perfect for: - API monetization - Premium content access - Metered API usage ### Do I need L402? L402 is optional. Use it if you want: - Per-request payment instead of subscriptions - Anonymous API access without accounts - Micropayment monetization ### Which plan includes L402? Every plan can create L402-protected endpoints; the Free Producer Sandbox is capped: | Plan | L402 Server-Side | Price | |------|------------------|-------| | Free Producer Sandbox | ✅ Yes (3 endpoints, capped monthly volume, 1,000 sats max/challenge) | Free — no card | | Agentic Commerce | ✅ Yes | $49/mo | | Agentic Commerce — Business | ✅ Yes | Contact us | Note: The MCP server's L402 *client* tools (`access_l402_resource`, `pay_l402_challenge`) are free for everyone. The subscription is only needed to *create* L402-protected endpoints. ### How do I check if my account has L402 enabled? Use the `/api/merchant/l402-status` endpoint: ```bash curl https://api.lightningenable.com/api/merchant/l402-status \ -H "X-API-Key: your-api-key" ``` Response shows `l402Enabled: true` if your plan includes L402 features. ### Can I monetize third-party APIs? Yes. The L402 proxy feature lets you wrap any API with Lightning payments. You pay the upstream API, customers pay you. ### Where is L402 used in production? [store.lightningenable.com](https://store.lightningenable.com) uses L402 payments for its Community Collection feature, where designers pay a Lightning micropayment to submit designs to the store. This demonstrates L402 as a gating mechanism for real-world commerce workflows. ## MCP Server (AI Integration) ### What is the MCP server? The MCP (Model Context Protocol) server enables AI agents like Claude to automatically pay for L402-protected APIs using Lightning payments. It connects to your wallet and handles payments on behalf of the AI. ### Are all MCP tools free? Yes. The MCP server is open-source (MIT) and free to install, and buying needs no subscription. Wallet, invoice, L402 client, budget, and API-discovery tools work with just a wallet. Three tools need a Lightning Enable merchant API key (`LIGHTNING_ENABLE_API_KEY`): `l402_producer` (sell access via L402), `renew_lightning_enable_subscription`, and the `agent_services` actions `request`, `publish`, `unpublish`, and `attest` (`discover`, `settle`, and `reputation` work with just a wallet). See the [MCP Complete Guide](/products/agentic-commerce/mcp-complete-guide) for the full tool list. ### What is LIGHTNING_ENABLE_API_KEY? Your merchant API key, set as an environment variable on the MCP server. Only the API-key tools above need it. ```json { "mcpServers": { "lightning-enable": { "env": { "NWC_CONNECTION_STRING": "nostr+walletconnect://...", "LIGHTNING_ENABLE_API_KEY": "your-merchant-api-key" } } } } ``` ### What happened to the license purchase? Removed in v1.6.0. The L402 client tools used to need a 6,000-sat license; they are now free. ### How do I test L402 payments? The [Lightning Enable Store](https://store.lightningenable.com) is a live L402-powered web store where AI agents can purchase physical merchandise. Ask Claude: ``` Buy me a Lightning Enable t-shirt from store.lightningenable.com ``` This demonstrates the full L402 flow: browse catalog, checkout (get 402), pay invoice, claim with L402 credential. ## API Features ### What is the X-Idempotency-Key header and when should I use it? The `X-Idempotency-Key` header prevents duplicate operations when retrying requests. If a network failure occurs and you're unsure whether your request succeeded, resending with the same idempotency key returns the original response instead of creating a duplicate. **How it works:** 1. Generate a unique key (UUID recommended) and include it as `X-Idempotency-Key: ` 2. If the request succeeds, the response is cached for 24 hours keyed to your merchant account 3. Repeating the request with the same key returns the cached response with an `X-Idempotency-Replayed: true` header **Supported endpoints:** - `POST /api/payments` (create payment) - `POST /api/refunds` (create refund) - `POST /api/checkout/sessions` (create checkout session) **Example:** ```bash curl -X POST https://api.lightningenable.com/api/payments \ -H "X-API-Key: your-api-key" \ -H "X-Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \ -H "Content-Type: application/json" \ -d '{"orderId": "ORDER-1", "amount": 10.00, "currency": "USD"}' ``` **Rules:** - Keys must be 256 characters or fewer - Keys are scoped per merchant, so different merchants can use the same key without conflict - Only successful responses (2xx) are cached - Use a new key for each distinct operation ### How do I trace a request through logs? Every API request is assigned a **correlation ID** for end-to-end tracing. You can use this ID when contacting support to quickly locate your request in our logs. **How it works:** - Send your own ID via the `X-Correlation-Id` request header, or the API generates one automatically - The same ID is returned in the `X-Correlation-Id` response header - All internal log entries for that request include the correlation ID **Example:** ```bash # Let the API generate one curl -v https://api.lightningenable.com/api/payments \ -H "X-API-Key: your-api-key" # Response header: X-Correlation-Id: 3fa85f64-5717-4562-b3fc-2c963f66afa6 # Or provide your own curl -v https://api.lightningenable.com/api/payments \ -H "X-API-Key: your-api-key" \ -H "X-Correlation-Id: my-trace-id-12345" ``` If something goes wrong, include the correlation ID when contacting support for faster diagnosis. ### Why am I getting a 429 Too Many Requests response? The API enforces rate limits to protect against abuse. If you exceed the limit, you'll receive a `429 Too Many Requests` response. **Rate limits by operation type:** | Operation | Limit | Window | |-----------|-------|--------| | Global (all requests) | 100 requests | 1 minute | | Payment/refund creation | 10 requests | 1 minute | | Checkout session creation | 5 requests | 1 minute | | Read operations (GET) | 200 requests | 1 minute | | Write operations (updates) | 20 requests | 1 minute | | Admin operations | 30 requests | 1 minute | | Webhooks | 100 requests | 1 minute (sliding window) | The global limit is counted per API key (or per IP address when unauthenticated). The tighter per-operation limits apply on top of it. **How to handle 429 responses:** 1. Implement exponential backoff (wait 1s, 2s, 4s, etc.) 2. Cache responses for repeated queries instead of re-requesting 3. Use webhooks for payment status instead of polling 4. Batch operations where possible ### What are the L402 proxy size limits? The L402 reverse proxy enforces size limits on both request and response bodies to prevent abuse: | Direction | Default Limit | |-----------|--------------| | Request body (client to proxy) | 1 MB (1,048,576 bytes) | | Response body (target API to client) | 10 MB (10,485,760 bytes) | - **Request too large:** Returns `413 Payload Too Large` - **Response too large:** Returns `502 Bad Gateway` These limits apply to proxied API calls only. Standard Lightning Enable API endpoints are not affected. ### What security protections does the L402 proxy have? The L402 proxy includes several security layers to protect both API providers and consumers: **SSRF (Server-Side Request Forgery) Protection:** - Target URLs are validated at request time by resolving DNS and checking the resulting IP addresses - Requests to private/internal IP ranges are blocked: `127.0.0.0/8`, `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, `169.254.0.0/16`, `::1`, `fc00::/7`, `fe80::/10` - Cloud metadata endpoints (e.g., `169.254.169.254`) are blocked to prevent credential theft **Header Sanitization:** - Sensitive hop-by-hop headers are stripped from forwarded requests (`Authorization`, `Proxy-Authorization`, `Connection`, etc.) - The proxy adds identification headers (`X-L402-Proxy-Id`, `X-L402-Proxy`) to proxied responses **Request Limits:** - Request and response body size limits (see "What are the L402 proxy size limits?") - 30-second timeout for upstream API responses (returns `504 Gateway Timeout` if exceeded) **Callback URL Validation:** - Webhook callback URLs are validated against blocked hostnames (localhost, metadata services, Kubernetes internals) - Common internal service ports (Redis 6379, PostgreSQL 5432, MySQL 3306, etc.) are blocked **Error Sanitization:** - In production, error responses are sanitized to remove stack traces, internal namespaces, file paths, and connection strings - Errors include a correlation ID for support reference without exposing implementation details ### How does error sanitization work in production? In production, the API automatically strips internal details from error responses to prevent information leakage. Fields like `stackTrace`, `innerException`, and `exceptionType` are removed. Fields like `message` and `details` are replaced with a generic message if they contain references to internal namespaces, file paths, or database details. Every error response includes a `correlationId` that you can reference when contacting support. In development environments, full error details (including stack traces) are returned for debugging convenience. ## Security ### Is my API key secure? Yes. API keys are encrypted at rest using AES-256-GCM. They're only decrypted in memory during authentication. ### Should I use HTTPS? Yes. Always use HTTPS in production. We require HTTPS for webhooks in production environments. ### What if my API key is compromised? 1. Generate a new API key immediately 2. Update your application configuration 3. The old key is automatically invalidated 4. Review your access logs ## Troubleshooting ### Payments not working 1. **Check API key** - Is it valid and for the correct environment? 2. **Check provider** - Is your payment provider account (Strike or OpenNode) active and verified? 3. **Check the key type** - Sandbox and testnet keys don't work on the hosted platform; use a production key 4. **Check logs** - What error is returned? ### Webhooks not received 1. **Check URL** - Is your webhook URL correct and HTTPS? 2. **Check firewall** - Can external services reach your endpoint? 3. **Test manually** - Use curl to test your endpoint ### Invalid API key errors 1. Verify key is copied correctly (no extra spaces) 2. Confirm environment matches (dev vs production) 3. Check key hasn't been revoked 4. Generate new key if needed ### Rate limit exceeded 1. Implement request caching 2. Use webhooks instead of polling 3. Add exponential backoff 4. Review the rate limits table in ["Why am I getting a 429 Too Many Requests response?"](#why-am-i-getting-a-429-too-many-requests-response) for specific limits per endpoint ## Support ### How do I get help? - **Documentation:** You're reading it! - **Email:** support@lightningenable.com - **Enterprise:** Contact support@lightningenable.com ### Is there an SLA? All plans include best-effort availability and priority email support. We do not currently offer formal SLA guarantees. ### Where can I report bugs? Email support@lightningenable.com with: - Description of the issue - Steps to reproduce - Error messages/logs - Your environment details ## Still Have Questions? If your question isn't answered here: 1. Check the [API Reference](/api-reference/overview) 2. Review the [Product Guides](/products/product-overview) 3. Email support@lightningenable.com We're here to help you succeed with Lightning payments! ============================================================================== # Activate with Lightning (L402 Fast Lane) Source: https://docs.lightningenable.com/getting-started/activate-with-lightning ============================================================================== # Activate with Lightning (L402 Fast Lane) **L402 Fast Lane** lets you create a Lightning Enable account and start a full 30-day **Agentic Commerce** trial by paying a single **100-sat L402 challenge** — no credit card. The signup itself runs on L402: `402 → pay → macaroon proof → account activated`. You use the exact payment flow your own APIs and agents will use. :::note Not the same as "Pay with Bitcoin — 10% off" That option pays a *subscription* with Bitcoin for a discount. **Fast Lane** is a *no-card trial* activated with a tiny Lightning payment — two different things. ::: :::tip What you'll need - A funded Lightning wallet with a little more than 100 sats (to cover the routing fee). L402 requires a wallet that surfaces the payment **preimage** — **LND, Strike, or an NWC wallet** (CoinOS, Alby Hub, CLINK). OpenNode and Primal NWC don't surface preimages and won't work here. - About a minute. ::: ## The fastest path: the MCP tool If you run the [Lightning Enable MCP](/products/agentic-commerce/mcp-quickstart) with a connected wallet, one tool call does everything: ```text create_lightning_enable_account(email="you@example.com") ``` It pays the 100-sat challenge, returns your merchant API key and trial details, and writes the key into `~/.lightning-enable/config.json` so the API-key-gated producer tools unlock on the next restart. Requires MCP **≥ 1.15.0** (`dotnet tool update -g LightningEnable.Mcp` or `pip install -U lightning-enable-mcp`). ## Dev CLI: l402-requests (Python) Prefer a script? The [`l402-requests`](/tools/l402-requests) client auto-detects your wallet and handles the whole `402 → pay → retry` handshake, preserving your email through the paid retry: ```python # pip install "l402-requests>=0.5.0" # 0.5.0 adds NIP-44 NWC support (Alby Hub, etc.) from l402_requests import L402Client client = L402Client() # auto-detects LND > NWC > Strike > OpenNode from env / config resp = client.post( "https://api.lightningenable.com/api/signup/l402", json={"email": "you@example.com"}, timeout=60, ) data = resp.json() print("API key: ", data["apiKey"]) # save this — it's shown once print("Trial ends:", data["trialEndsAt"]) print("Dashboard: ", data["dashboardUrl"]) ``` The [.NET](/tools/l402-dotnet) and [TypeScript](/tools/l402-ts) clients do the same handshake — point them at the same endpoint with a JSON `{ "email": "…" }` body. ## Raw protocol (any language) Before spending anything, you can fetch a read-only quote — `GET https://api.lightningenable.com/api/signup/l402` returns availability, the current price and trial terms, required fields, and the exact flow, with no side effects. The endpoint itself is a standard two-phase L402 flow, so you can drive it by hand: 1. **Ask for the challenge** — `POST /api/signup/l402` with body `{"email":"you@example.com"}`. You get `402 Payment Required` with a `WWW-Authenticate: L402 macaroon="…", invoice="…"` header. 2. **Pay the invoice** with any preimage-surfacing wallet to obtain the preimage. 3. **Complete** — repeat the POST with the *same* body plus `Authorization: L402 :`. You get `200 OK` and your account: ```json { "status": "created", "apiKey": "lgw_…", "merchantId": 123, "email": "you@example.com", "planTier": "individual", "subscriptionStatus": "trialing", "trialEndsAt": "2026-08-06T00:00:00Z", "dashboardUrl": "https://api.lightningenable.com/dashboard", "nextSteps": [ { "id": "store_api_key", "title": "Store your API key", "detail": "…", "url": "https://api.lightningenable.com/dashboard/settings" }, { "id": "connect_payment_provider", "title": "Connect a payment provider", "detail": "…", "method": "PUT", "endpoint": "/api/merchant/strike-key", "url": "https://api.lightningenable.com/dashboard/login", "docs": "https://docs.lightningenable.com/strike-setup/account-setup" }, { "id": "mint_first_challenge", "title": "Mint your first L402 challenge", "detail": "…", "method": "POST", "endpoint": "/api/l402/challenges", "docs": "https://docs.lightningenable.com/products/agentic-commerce/producer-api-reference" }, { "id": "trial", "title": "Manage your trial", "detail": "…", "url": "https://api.lightningenable.com/dashboard" }, { "id": "docs", "title": "Read the full guide", "detail": "…", "url": "https://docs.lightningenable.com/getting-started/activate-with-lightning", "docs": "https://api.lightningenable.com/llms.txt" } ], "instructions": "Your Lightning Enable account is ready. 1) Store your API key now — it is shown only in this response… (full plain-text walkthrough of the same five steps)" } ``` `nextSteps` is an ordered array (always these five ids, in this order) for callers that branch on structured data; `instructions` is the same guidance as plain text for callers that only relay text. A welcome email also goes to the signup address. ## Semantics worth knowing (all activation paths) These apply whether you used the MCP tool, the Python client, or the raw protocol: - **Enumeration-safe by design:** the challenge phase never reveals whether an email is registered — an already-registered email mints an identical 402. If the email already has an account, the **paid** verify returns `409 merchant_exists` and the ~100-sat fee is not refunded, so confirm the email is correct and new before paying. Agents: always use the operator's real email, never an invented one. - **Verify promptly after paying.** The credential expires (about an hour). A late verify returns `401` — indistinguishable from an invalid token — and re-POSTing mints a **new payable invoice**; the first payment is not automatically recovered. If you paid, complete the retry right away. An unpaid or abandoned challenge never creates an account, and an unpaid `401` costs nothing — just re-POST for a fresh challenge. - **The API key is shown once** in the success response. It is recoverable later by signing in at the dashboard via a magic-link email to your signup address — so the email must be real and reachable. (Regenerating the key from Settings invalidates the old one immediately.) - **A welcome email is sent** to the signup address right after the account is created — the same guidance as the response's `nextSteps`/`instructions`, written for the human behind the agent. It never contains the API key or the L402 macaroon. Sending is best-effort: a delivery problem never fails or delays the signup response, and it is sent exactly once per created account (not on a `409 merchant_exists` retry). ## First success after activation Prove the account works end-to-end for about 1 sat, without building an API first: 1. Save the returned `apiKey` securely. 2. **Connect your payment provider** — add your [Strike](/strike-setup/account-setup) (recommended) or OpenNode API key, or an [NWC wallet connection](/nwc-setup/account-setup), in the [dashboard](https://api.lightningenable.com/dashboard) Settings. Challenges settle to *your* provider account; until one is connected, challenge creation returns `400 payment_provider_not_configured`. 3. Create a 1-sat challenge: `POST /api/l402/challenges` (or the `l402_producer` MCP tool with `action="create"`) with any resource name and `priceSats: 1`. 4. Pay your own challenge with your wallet (`pay_l402_challenge`). 5. Verify it: `POST /api/l402/challenges/verify` (or `l402_producer` with `action="verify"`) — a `valid: true` response confirms your producer account is fully provisioned. ## What you get - A **30-day Agentic Commerce** trial: unlimited L402 endpoints, per-endpoint pricing, and the live dashboard with a per-request payment feed. - Your **merchant API key** — save it, it's shown once. It unlocks the [L402 Producer API](/products/agentic-commerce/l402-producer-api) and the API-key-gated MCP producer/ASA tools. - Sign in anytime at the [dashboard](https://api.lightningenable.com/dashboard). ## After the trial Add billing to keep Agentic Commerce, or do nothing and your account automatically moves to the **Free Producer Sandbox** (3 endpoints, limited monthly challenge volume, 1,000-sat max challenge price). Either way you keep your account and endpoints — prefer the no-code path? Start the Sandbox directly at [api.lightningenable.com/dashboard/signup](https://api.lightningenable.com/dashboard/signup). To keep Agentic Commerce without a card, pay for it in sats with the same L402 flow you used to sign up. See [Renew in sats](#renew-in-sats). The Sandbox's **3 endpoints** means 3 *distinct resource paths*, counted for the life of the account — unlike the monthly challenge volume, this tally doesn't reset. Reuse your paths while experimenting rather than minting a fresh one each time. If a throwaway or test path ends up permanently holding a slot, email [support@lightningenable.com](mailto:support@lightningenable.com) and we can retire it to free the slot. ## Renew in sats Agentic Commerce costs $49 a month by card, or a sats price per 30-day period paid over Lightning with an L402 challenge. It's the same plan with the same features; only the way you pay differs. The sats price isn't stated on this page: read it from the quote, `GET /api/merchant/subscription/renew` (see [Check the price first](#check-the-price-first)), which returns it in `priceSats`. The sats price is configuration and can change; a challenge you already hold is honored at the price it was issued at unless the price has gone up, in which case mint a fresh one. ### Who can renew in sats You can pay for Agentic Commerce in sats if your account is one of these: - A Free Producer Sandbox account. - A Fast Lane trial account. If you pay during the trial, the 30 days start when the trial would have ended. - A sats-paid account, whether its period is still running or has lapsed. - An account whose card subscription is canceled (Stripe status `canceled` or `incomplete_expired`). Paying in sats moves the account off Stripe: the Stripe subscription ID is cleared and the Stripe customer ID is kept. - An account paid through the legacy Bitcoin checkout, while its Bitcoin period is still running. The 30 days start when that period ends. Once the Bitcoin period lapses, the legacy job deactivates the account and its API key stops working, so it can't reach the renew endpoint; email [support@lightningenable.com](mailto:support@lightningenable.com) to restore it. You can't renew in sats while the account has a card subscription that isn't canceled, including one that is trialing, past due, unpaid, incomplete, or paused (cancel it in the billing portal first, so the card is never charged alongside your sats), on Agentic Commerce — Business (email [support@lightningenable.com](mailto:support@lightningenable.com)), or on an account an operator has disabled. Both calls need your merchant API key in the `X-API-Key` header, and you need a wallet that surfaces the payment preimage, the same wallets listed at the top of this page. ### The fastest path: the MCP tool If you run the [Lightning Enable MCP](/products/agentic-commerce/mcp-quickstart) with a connected wallet and your merchant API key configured, one tool call pays the renewal: ```text renew_lightning_enable_subscription(maxSats=) ``` `maxSats` is the most this call may pay; the server sets the price. Set it to at least the `priceSats` that [the quote](#check-the-price-first) returns. The Python server spells it `max_sats`. The `standard` and `full` profiles list the tool; under `lite` it isn't listed but stays callable by name. It requires MCP **2.1.0** or later. If `maxSats` is above your auto-approve threshold, the tool returns `requiresConfirmation: true` and the server sends a confirmation code to the human operator over its approval channel (its console by default). The code never appears in a tool result. Ask the human for the code, then call the tool again with the same `maxSats` and `confirmationNonce=""` (`confirmation_nonce` in Python). On success the tool returns the paid-through date, the amount, and the payment hash. Sats amounts in these examples are shown as placeholders such as ``. In a real response they're integers, and the live renewal price is whatever `GET /api/merchant/subscription/renew` returns in `priceSats`. ```json { "success": true, "currentPeriodEnd": "2026-11-10T14:03:22Z", "paidSats": , "paymentHash": "9f2c4e7a1b3d5f60718293a4b5c6d7e8f9012a3b4c5d6e7f8091a2b3c4d5e6f7a", "alreadyCredited": false, "planTier": "individual" } ``` Every call to the tool starts with a fresh challenge, so a second call buys a second period. ### Dev CLI: l402-requests (Python) The [`l402-requests`](/tools/l402-requests) client handles the `402 → pay → retry` handshake and keeps your headers on the retry. Send `X-API-Key` and a `POST` with an empty body. The client's default budget caps a single payment at 1,000 sats, so read the renewal price from the quote and raise the budget to cover it: ```python import requests from l402_requests import BudgetController, L402Client quote = requests.get( "https://api.lightningenable.com/api/merchant/subscription/renew", headers={"X-API-Key": "le_merchant_abc123"}, timeout=30, ).json() price = quote["priceSats"] # the live renewal price budget = BudgetController( max_sats_per_request=price, max_sats_per_hour=price, max_sats_per_day=price * 2, ) client = L402Client(budget=budget) # auto-detects LND > NWC > Strike > OpenNode resp = client.post( "https://api.lightningenable.com/api/merchant/subscription/renew", headers={"X-API-Key": "le_merchant_abc123"}, timeout=60, ) data = resp.json() print("Paid through:", data["currentPeriodEnd"]) print("Paid sats: ", data["paidSats"]) ``` An `L402Client` caches the token it paid for. A second `post` to the same URL on the same client replays that token and answers `alreadyCredited: true` rather than buying another period. To prepay another period, create another client. ### Raw protocol (any language) The endpoint is a standard two-phase L402 flow: 1. **Ask for the challenge.** `POST /api/merchant/subscription/renew` with your API key and no `Authorization` header. You get `402 Payment Required` with a `WWW-Authenticate: L402 macaroon="…", invoice="…"` header and this body: ```json { "invoice": "lnbc…", "macaroon": "AgEB…", "paymentHash": "9f2c4e7a1b3d5f60718293a4b5c6d7e8f9012a3b4c5d6e7f8091a2b3c4d5e6f7a", "expiresAt": "2026-10-11T15:03:22Z", "priceSats": , "periodDays": 30, "currentPeriodEnd": null, "periodEndIfPaid": "2026-11-10T14:03:22Z", "instructions": "Pay the Lightning invoice, then retry this request with Authorization: L402 :." } ``` 2. **Pay the invoice** before `expiresAt` with a wallet that returns the preimage. 3. **Complete.** Repeat the `POST` with your API key and `Authorization: L402 :`. You get `200 OK`: ```json { "success": true, "alreadyCredited": false, "planTier": "individual", "currentPeriodEnd": "2026-11-10T14:03:22Z", "paidSats": , "paymentHash": "9f2c4e7a1b3d5f60718293a4b5c6d7e8f9012a3b4c5d6e7f8091a2b3c4d5e6f7a" } ``` The same two calls with `curl`: ```bash # 1. Get the challenge curl -i -X POST https://api.lightningenable.com/api/merchant/subscription/renew \ -H "X-API-Key: le_merchant_abc123" # 3. Complete, after paying the invoice curl -X POST https://api.lightningenable.com/api/merchant/subscription/renew \ -H "X-API-Key: le_merchant_abc123" \ -H "Authorization: L402 :" ``` Every `POST` without an `Authorization` header mints a fresh challenge with a new invoice, so a second challenge buys a second period. You can request up to 5 challenges a minute per API key; past that the endpoint answers `429`. Redeeming a token has its own, larger allowance, so asking for challenges never blocks a token you've paid. The token is bound to your account. It can't renew another account, and another account's API key can't redeem it. It stays redeemable for 7 days after the challenge is issued. Each payment is credited once. Sending the same token again returns `200` with `alreadyCredited: true` and the paid-through date that payment produced; it never charges you or extends the period a second time. That date can be earlier than your current paid-through date if you've paid again since. The success body never contains the macaroon or the preimage. ### Check the price first `GET /api/merchant/subscription/renew` returns the price, the period, whether your account can renew, and the paid-through date a payment would produce. It creates no invoice and has no side effects, so an agent can call it before deciding to pay: ```bash curl https://api.lightningenable.com/api/merchant/subscription/renew \ -H "X-API-Key: le_merchant_abc123" ``` ```json { "eligible": true, "reason": null, "priceSats": , "periodDays": 30, "paymentMethod": "l402", "currentPeriodEnd": "2026-11-10T14:03:22Z", "periodEndIfPaid": "2026-12-10T14:03:22Z", "endpoint": "/api/merchant/subscription/renew" } ``` When `eligible` is `false`, `reason` says why. See the [Merchant Settings API](/api-reference/merchant-settings#renew-with-lightning-l402) for every field. ### What happens at the end of the period Nothing charges you automatically. Once your paid-through date passes, authenticated API calls answer `403` with `action_required: "renew_subscription"` and the renew endpoint: ```json { "error": "Subscription period expired", "message": "Your Lightning-paid subscription period has ended. Renew now with an L402 payment, or the account moves to the Free Producer Sandbox within 24 hours.", "subscription_status": "active", "current_period_end": "2026-11-10T14:03:22.0000000Z", "action_required": "renew_subscription", "renew": { "endpoint": "/api/merchant/subscription/renew" } } ``` A daily job (03:30 UTC) then moves the account to the Free Producer Sandbox and sends you an email. Your API key keeps working within the Free limits and the account stays active, so you can pay again at any time. A payment after the period has lapsed starts its 30 days from the moment you pay. The renew endpoint is exempt from subscription enforcement, so a lapsed account can always reach it. ### Prepaying Each payment adds 30 days to the later of today and your current paid-through date. You can keep paying ahead while your paid-through date is no more than 365 days away; past that, asking for a new challenge answers `409 prepaid_cap_reached`. The cap applies when you ask for a challenge, not when you redeem one. A token you've already paid is credited even if another payment has since moved your paid-through date past 365 days. Each token stays redeemable for 7 days after its challenge is issued. ### Where the sats go Your payment settles with the payment provider of the Lightning Enable account that receives renewals. Lightning Enable does not hold funds; the payment provider facilitates custody and settlement. ### Troubleshooting | Status and code | What it means | What to do | |---|---|---| | `409 stripe_subscription_active` | This account has a card subscription that isn't canceled (for example active, trialing, past due, or unpaid). | Cancel it in the billing portal, then renew in sats. | | `409 business_plan_contact_support` | The account is on Agentic Commerce — Business. | Email [support@lightningenable.com](mailto:support@lightningenable.com). | | `409 prepaid_cap_reached` | You asked for a challenge while already paid more than 365 days ahead. The body carries `currentPeriodEnd`. A token you've already paid is still credited. | Wait until your paid-through date is within 365 days. | | `429` | More than 5 challenge requests a minute from this API key. | Wait a minute, or redeem the challenge you already have. | | `403 account_disabled` | An operator disabled the account. | Email [support@lightningenable.com](mailto:support@lightningenable.com). | | `401 invalid_l402_token` | The token was minted for another account, has expired, was altered, doesn't match a renewal challenge, or was minted at a price lower than the configured price. | `POST` without an `Authorization` header for a fresh challenge. | | `400 invalid_authorization` | The `Authorization` header isn't `L402 :`. | Fix the header, or omit it to get a challenge. | | `503 renewal_contended` | Another request was updating your account. Your payment is safe. | Retry the same request with the same `Authorization` header after the `Retry-After` delay. It's credited at most once. | | `503 upstream_unavailable` or `503 challenge_persist_failed` | No invoice was issued. | Retry shortly. | | `503 l402_subscription_unavailable` or `503 l402_subscription_misconfigured` | Lightning renewals are switched off or not set up on this deployment. | Retry later, or email [support@lightningenable.com](mailto:support@lightningenable.com). | If a `409`, `403`, or `503` arrives after you paid, keep the token. Nothing was credited, and the same token stays redeemable, until it expires 7 days after its challenge was issued, once the cause is fixed. A `401 invalid_l402_token` is different: a token that was altered, minted for another account, or already credited to another account is refused permanently. If you paid a challenge minted before a price increase, that payment is never credited; email [support@lightningenable.com](mailto:support@lightningenable.com) with the payment hash. See the [error reference](/api-reference/errors#lightning-renewal-errors) for the full response bodies. ## Next step: sell with your agent You have an API key and a producer account — the next step is turning that into a paid endpoint. **[Sell With Your Agent](./sell-with-your-agent)** picks up right here: give your agent a receive wallet, a spend ceiling, and the API you want to charge for, and the [`producer-setup` skill](https://github.com/refined-element/lightning-enable-skills/tree/main/skills/producer-setup) wires up the whole path — proxy, manifest, and a self-test payment that proves it works — with no dashboard clicking required. --- *Lightning Enable is API middleware and never holds your funds. Your wallet pays the challenge directly, and your chosen settlement provider (Strike or OpenNode) custodies every payment on the APIs you monetize.* ============================================================================== # The Lightning Enable Ecosystem Source: https://docs.lightningenable.com/getting-started/ecosystem ============================================================================== # The Lightning Enable Ecosystem **Lightning Enable — infrastructure for agent commerce over Lightning.** Lightning Enable is a suite of open-source tools and a commerce orchestration layer that gives platforms, developers, and AI agents everything they need to transact over the Lightning Network using the L402 protocol. ## Architecture ``` +-------------------------------+ | Payment Providers | | Strike . OpenNode . NWC | | custody / settlement / KYB | +---------------+---------------+ | +---------------+---------------+ | Lightning Enable API | | api.lightningenable.com | | orchestration / L402 proxy | | / webhooks | +---------------+---------------+ | +-------------------------+-------------------------+ | | | +-------+--------+ +-------+--------+ +-------+--------+ | MCP Server | | L402 HTTP | | Agent Commerce | | AI agent tools | | Clients | | Store | | NuGet / PyPI / | | Python .NET TS | | API marketplace| | Docker | | (consumer) | | L402 data APIs | +-------+--------+ +----------------+ +----------------+ | +-------+--------+ +---------------------------------------+ | NostrWolfe | | L402 Server SDKs (producer side) | | Agent relay | | l402-server, l402-express (npm) | | wss://agents. | | L402Server, L402Server.AspNetCore | | lightningenable| | (NuGet) - charge for your own API | | .com | | on your own domain | +----------------+ +---------------------------------------+ ``` ## Components ### Agentic Commerce Platform The commerce orchestration layer that connects platforms to payment providers (Strike, OpenNode, or the merchant's own wallet over NWC) for Bitcoin Lightning payments. Handles invoices, webhooks, L402 proxy, and merchant management. - **Live API:** [api.lightningenable.com](https://api.lightningenable.com) - **Docs:** [docs.lightningenable.com](https://docs.lightningenable.com) - **Get started:** [Quick Start Guide](/getting-started/quick-start) Lightning Enable does not hold funds — the configured payment provider facilitates custody and settlement. ### MCP Server — AI Agent Tools Open-source (MIT) MCP server that gives AI agents a Lightning wallet — wallet, invoice, and L402 tools free out of the box, plus producer and Agent Service Agreement tools with a Lightning Enable API key. See the [MCP Complete Guide](/products/agentic-commerce/mcp-complete-guide) for the full tool list. - **GitHub:** [refined-element/lightning-enable-mcp](https://github.com/refined-element/lightning-enable-mcp) - **NuGet:** [LightningEnable.Mcp](https://www.nuget.org/packages/LightningEnable.Mcp) - **PyPI:** [lightning-enable-mcp](https://pypi.org/project/lightning-enable-mcp) - **Docker Hub:** [refinedelement/lightning-enable-mcp](https://hub.docker.com/r/refinedelement/lightning-enable-mcp) - **Docs:** [MCP Quick Start](/products/agentic-commerce/mcp-quickstart) ```bash # Install in one command dotnet tool install -g LightningEnable.Mcp # .NET pip install lightning-enable-mcp # Python (includes Nostr Wallet Connect support) docker pull refinedelement/lightning-enable-mcp # Docker ``` ### L402 HTTP Clients — Auto-Paying Libraries Open-source (MIT) drop-in HTTP clients that handle L402 payment flows automatically. Three lines of code — paid APIs just work. | Language | Package | Install | |----------|---------|---------| | **Python** | [l402-requests](https://pypi.org/project/l402-requests) | `pip install l402-requests` | | **.NET** | [L402Requests](https://www.nuget.org/packages/L402Requests) | `dotnet add package L402Requests` | | **TypeScript** | [l402-requests](https://www.npmjs.com/package/l402-requests) | `npm install l402-requests` | - **Python docs:** [l402-requests](/tools/l402-requests) - **.NET docs:** [L402Requests](/tools/l402-dotnet) - **TypeScript docs:** [l402-requests](/tools/l402-ts) ### L402 Server SDKs — Charge for Your API (Producer Side) Open-source (MIT) server SDKs and framework middleware for the **producer** side of L402: drop one line of middleware into your existing API and charge per request over Lightning. Native mode — your API stays on your own domain, and traffic never flows through Lightning Enable. | Stack | SDK | Framework middleware | Install | |-------|-----|---------------------|---------| | **Node.js** | [l402-server](https://www.npmjs.com/package/l402-server) | [l402-express](https://www.npmjs.com/package/l402-express) (Express) | `npm install l402-express` | | **.NET** | [L402Server](https://www.nuget.org/packages/L402Server) | [L402Server.AspNetCore](https://www.nuget.org/packages/L402Server.AspNetCore) (ASP.NET Core) | `dotnet add package L402Server.AspNetCore` | - **Docs:** [Native Integration overview](/products/agentic-commerce/native-integration) · [Express walkthrough](/products/agentic-commerce/native-integration-express) · [ASP.NET Core walkthrough](/products/agentic-commerce/native-integration-aspnet) **Live reference apps** — open-source paid APIs you can curl to see a 402 in your terminal: - **Express + Node:** [refined-element/l402-example-node](https://github.com/refined-element/l402-example-node) - **ASP.NET Core:** [refined-element/l402-example-aspnet](https://github.com/refined-element/l402-example-aspnet) ### Claude Skills — Ready-Made Agentic Commerce Skills Open-source Claude Skills for agentic commerce over Lightning + L402, powered by the Lightning Enable MCP server. Runs in Claude Code and Claude Desktop. - **GitHub:** [refined-element/lightning-enable-skills](https://github.com/refined-element/lightning-enable-skills) - Requires the [MCP server](#mcp-server--ai-agent-tools) with a configured wallet ### Research Agent Demo — Agent Buys Its Own Data Reproducible demo of end-to-end agent commerce: an AI agent buys its own research data over Lightning (L402) and writes a cited paper — about 20 sats, no API keys, no accounts. - **GitHub:** [refined-element/l402-research-agent](https://github.com/refined-element/l402-research-agent) ### Agent Commerce Store — L402 API Marketplace Live marketplace of dozens of L402 data APIs payable via Lightning micropayments — weather, research papers, SEC filings, economic data, and more, all accessible by AI agents. See the live manifest for the current list. - **Live:** [agent-commerce.store](https://agent-commerce.store) - **Manifest:** [L402 Manifest](https://agent-commerce.store/.well-known/l402-manifest.json) - **Machine docs:** [llms.txt](https://agent-commerce.store/llms.txt) ### Lightning Enable Store — Live L402 Demo Physical merch store demonstrating L402 checkout. AI agents can browse, pay via Lightning, and receive real products. - **Live:** [store.lightningenable.com](https://store.lightningenable.com) - **Machine docs:** [llms-full.txt](https://store.lightningenable.com/llms-full.txt) ### NostrWolfe — Agent Discovery via Nostr Open protocol enabling AI agents to discover each other, request services, and settle via the Lightning Network (L402) over Nostr. - **Website:** [nostrwolfe.com](https://nostrwolfe.com) - **Agent Relay:** `wss://agents.lightningenable.com` - **SDKs:** [le-agent-sdk-python](https://github.com/refined-element/le-agent-sdk-python) (Python) · [le-agent-sdk-ts](https://github.com/refined-element/le-agent-sdk-ts) (TypeScript) · [le-agent-sdk-dotnet](https://github.com/refined-element/le-agent-sdk-dotnet) (.NET) ## Start Here If You Want To... | Goal | Start here | |------|-----------| | **Accept Lightning payments on your platform** | [Quick Start Guide](/getting-started/quick-start) | | **Give your AI agent a Lightning wallet** | [MCP Quick Start](/products/agentic-commerce/mcp-quickstart) | | **Monetize your API with pay-per-request** | [API Monetization](/products/agentic-commerce/api-monetization) | | **Have an agent set up a paid endpoint for you, hands-free** | [Sell With Your Agent](/getting-started/sell-with-your-agent) | | **Charge for your API on your own domain (native mode)** | [Native Integration](/products/agentic-commerce/native-integration) | | **See a working paid API end to end** | [l402-example-node](https://github.com/refined-element/l402-example-node) · [l402-example-aspnet](https://github.com/refined-element/l402-example-aspnet) | | **Access L402 APIs from Python/.NET/TypeScript** | [L402 HTTP Clients](/tools/l402-requests) | | **Give Claude ready-made commerce skills** | [lightning-enable-skills](https://github.com/refined-element/lightning-enable-skills) | | **Watch an agent buy its own research data** | [l402-research-agent](https://github.com/refined-element/l402-research-agent) | | **Browse available paid APIs** | [Agent Commerce Store](https://agent-commerce.store) | | **Let agents discover each other on Nostr** | [NostrWolfe](https://nostrwolfe.com) | ## Links | Resource | URL | |----------|-----| | API | [api.lightningenable.com](https://api.lightningenable.com) | | Documentation | [docs.lightningenable.com](https://docs.lightningenable.com) | | Product Page | [lightningenable.com](https://lightningenable.com) | | A-Commerce Manifesto | [a-commerce.lightningenable.com](https://a-commerce.lightningenable.com) | | Parent Company | [refinedelement.com](https://refinedelement.com) | | Support | [support@lightningenable.com](mailto:support@lightningenable.com) | ============================================================================== # First Payment Source: https://docs.lightningenable.com/getting-started/first-payment ============================================================================== # Your First Payment This tutorial walks you through creating, displaying, and confirming your first Lightning Network payment using Lightning Enable. ## What We'll Build A simple payment flow that: 1. Creates a Lightning invoice 2. Displays a QR code for payment 3. Polls for payment confirmation 4. Shows a success message ## Prerequisites - Lightning Enable merchant API key (Free Producer Sandbox or a paid plan) - Payment provider API key configured in your merchant account (Strike or OpenNode) - Basic HTML/JavaScript knowledge ## Step 1: Create the Payment (server-side) :::danger Never put your API key in browser code `X-API-Key` is your merchant secret — anyone who sees it can create invoices and read your payment data. Always call the Lightning Enable API from **your backend**. (Browser calls would also fail: api.lightningenable.com only allows cross-origin requests from allowlisted origins.) ::: Create the invoice from your server, then hand only the invoice details to the browser: ```javascript // server.js (Node/Express) — your backend, where the API key stays secret app.post('/create-payment', async (req, res) => { const response = await fetch('https://api.lightningenable.com/api/payments', { method: 'POST', headers: { 'X-API-Key': process.env.LIGHTNING_ENABLE_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ orderId: req.body.orderId, amount: 25.00, // price from YOUR catalog — never trust the client currency: 'USD', description: 'Order ' + req.body.orderId, successUrl: 'https://yoursite.com/success' }) }); if (!response.ok) { return res.status(502).json({ error: 'Failed to create payment' }); } const payment = await response.json(); // Send the browser only what it needs to display res.json({ invoiceId: payment.invoiceId, lightningInvoice: payment.lightningInvoice, expiresAt: payment.expiresAt }); }); ``` The response includes everything you need: ```json { "invoiceId": "1042", "status": "unpaid", "lightningInvoice": "lnbc250n1...", "onchainAddress": "bc1q...", "hostedCheckoutUrl": "https://checkout.opennode.com/...", // OpenNode only "payUrl": "https://api.lightningenable.com/pay/AbC...xyz", // hosted payment page, any provider "expiresAt": "2026-07-03T13:00:00Z" // expiry varies by provider — always read this field } ``` ## Step 2: Display the QR Code Use a QR code library to display the Lightning invoice: ```html Pay with Lightning

Pay $25.00

Scan with your Lightning wallet

Loading...
Waiting for payment...
``` ## Step 3: Poll for Payment Status The browser can poll the **public status endpoint** — it requires no API key and returns only `{ "status": "..." }`, so it is safe to call from client-side code: ```javascript // Browser-safe: no API key needed. Returns { "status": "unpaid" | "processing" | "paid" | "expired" } async function checkPaymentStatus(invoiceId) { const response = await fetch( `https://api.lightningenable.com/api/payments/${invoiceId}/status` ); return await response.json(); } function startPolling(invoiceId) { const statusEl = document.getElementById('status'); const interval = setInterval(async () => { const payment = await checkPaymentStatus(invoiceId); switch (payment.status) { case 'paid': statusEl.className = 'status status-paid'; statusEl.textContent = 'Payment received!'; clearInterval(interval); // Redirect to success page (pass your own order reference) window.location.href = '/success?invoice=' + invoiceId; break; case 'expired': statusEl.className = 'status status-expired'; statusEl.textContent = 'Invoice expired'; clearInterval(interval); break; case 'processing': statusEl.textContent = 'Payment detected, confirming...'; break; } }, 3000); // Check every 3 seconds } // Start polling when page loads startPolling('1042'); ``` :::tip Fulfillment belongs on the server Client-side polling is for UX only. Fulfill orders from your **webhook handler** (see below) or by checking `GET /api/payments/{invoiceId}` from your backend with your API key — never trust the browser to tell you an order was paid. ::: ## Step 4: Handle Success When the payment is confirmed, redirect to a success page: ```html Payment Successful

Payment Successful!

Thank you for your payment.

Invoice:

``` ## Complete Working Example Here's a complete, working HTML file you can use: ```html Lightning Payment Demo

Complete Payment

$25.00 USD
Lightning
Bitcoin
Hosted
Waiting for payment...
``` ## Using Webhooks Instead of Polling For production, webhooks are more reliable than polling: ```javascript // Express.js webhook handler app.post('/webhooks/lightning', express.raw({type: '*/*'}), (req, res) => { const signatureHeader = req.headers['x-lightningenable-signature']; const payload = req.body.toString(); // Parse t={timestamp},v1={hmac} format const parts = Object.fromEntries( signatureHeader.split(',').map(p => p.split('=')) ); const timestamp = parts['t']; const receivedHmac = parts['v1']; // Verify signature: HMAC-SHA256 of "{timestamp}.{payload}" if (!verifySignature(timestamp, payload, receivedHmac, process.env.WEBHOOK_SECRET)) { return res.status(401).send('Invalid signature'); } const data = JSON.parse(payload); if (data.status === 'paid') { // Process the order processOrder(data.orderId, data.invoiceId); } res.status(200).send('OK'); }); ``` ## Testing The hosted platform connects to your provider's **production** API, so sandbox and testnet keys don't work. Two ways to test: 1. **Demo invoice:** send `amount: 0`. The invoice auto-confirms without a real payment, so you can exercise polling and webhooks. 2. **Small real payment:** create a $1 invoice and pay it from any Lightning wallet. The funds land in your own provider account. See [Testing Your Strike Integration](/strike-setup/testing) for the full checklist. ## Next Steps - [Webhooks Implementation](/api-reference/webhooks) - Instant payment notifications - [Error Handling](/api-reference/errors) - Handle edge cases ## Stuck? Start with [Troubleshooting](/troubleshooting) for the common failures and their fixes. If something's broken or not working, email support@lightningenable.com — a real person reads it. ============================================================================== # Your First Settlement Source: https://docs.lightningenable.com/getting-started/first-settlement ============================================================================== # Your First Settlement In Lightning Enable, settlements and payments follow the same flow. A "settlement" is simply a completed payment that has been settled to your payment provider account. For the complete guide on creating and processing payments (which result in settlements), see **[Your First Payment](./first-payment.md)**. ## Settlement vs Payment - **Payment**: A Lightning invoice has been created and is awaiting payment - **Settlement**: The payment has been confirmed and funds are settled to your provider account Your payment provider (Strike or OpenNode) handles the settlement process automatically. Check your provider dashboard for settlement details and timing. ============================================================================== # Introduction Source: https://docs.lightningenable.com/ ============================================================================== > **Lightning Enable — infrastructure for agent commerce over Lightning.** *See the [full ecosystem map](/getting-started/ecosystem) for every component, package, and live demo.* # Introduction to Lightning Enable **Lightning Enable is infrastructure for agent commerce over Lightning.** It lets autonomous agents discover, buy, and get paid for digital work — over the Lightning Network, within the budgets and rules their humans control. AI agents are starting to buy. They call APIs, run workflows, and pay for tools. But card rails assume a human identity, a checkout flow, and a billing address — none of which an agent workflow has. Lightning per-request payments fit: an agent hits a paid endpoint, gets an HTTP `402` with a Lightning invoice, pays it in about a second, and retries with cryptographic proof. It gets in; you get paid. Lightning Enable is the software that makes an agent and an API speak that language — with no Lightning node, no hosted middleman, and no per-transaction cut. :::info Lightning Enable never holds your funds Lightning Enable is API middleware. **Lightning Enable does not hold funds — your payment provider (Strike or OpenNode, or your own wallet over NWC) facilitates custody and settlement.** Every payment your API earns settles directly into *your own* account. Lightning Enable charges a flat monthly subscription, never a percentage of your revenue. ::: ## Two sides of the same rail Agent commerce has a buyer and a seller. Lightning Enable serves both on one rail — pick the side you're on: ``` BUY side (agents / consumers) SELL side (producers / API builders) ┌───────────────────────────────────┐ ┌───────────────────────────────────────┐ │ Free, open-source MCP server │ │ Create L402 producer endpoints │ │ Bring your own wallet │ │ Return HTTP 402 + Lightning invoice │ │ (Strike · NWC · LND) │ │ Verify the payment proof │ │ Pay & access any L402 API │ │ Get paid per request, in sats │ └────────────────┬──────────────────┘ └────────────────────┬──────────────────┘ │ same L402 rail, same settlement │ └────────────────────┬───────────────────────┘ ▼ Strike / OpenNode (custody + settlement) ``` ### For agents (the buy side) Agents pay with the **free, open-source [MCP server](/products/agentic-commerce/mcp-quickstart)** (MIT — a compact action-based tool set, with a minimal lite profile for pay-only agents; most tools work out of the box with no API key — see the [MCP Complete Guide](/products/agentic-commerce/mcp-complete-guide) for the full breakdown). Connect your own wallet — Strike, an NWC wallet, or LND — and your agent can pay Lightning invoices, access L402-protected APIs, and enforce spending budgets. No hosted MCP and no Lightning node required. Using the MCP as a buyer is **always free**. ### For producers (the sell side) Producers turn any HTTP API into a paid one. Gate a route, return a `402` challenge, and get paid per request in real Bitcoin — while your existing subscriptions, rate limits, and API keys stay exactly as they are. Lightning per-request runs *alongside* what you already charge; it's an additional revenue stream, not a replacement. This is the part that needs a Lightning Enable plan (or the free sandbox below). ## Fastest path to first success Pick your side and get a real payment moving in minutes. **If you're building an agent:** install the MCP, connect a wallet, and run the built-in self-test. ```bash # .NET tool (recommended) dotnet tool install --global LightningEnable.Mcp # or Python pip install lightning-enable-mcp ``` Then have your agent call **`test_l402_payment`** — it pays a tiny live L402 challenge end-to-end and confirms your wallet is wired up correctly. Full walkthrough: [MCP Quick Start](/products/agentic-commerce/mcp-quickstart) · tool reference: [MCP Complete Guide](/products/agentic-commerce/mcp-complete-guide). **If you're monetizing an API:** start the **Free Producer Sandbox** (no card), create your first L402 endpoint, and watch an agent pay it. - Start free: [api.lightningenable.com/dashboard/signup](https://api.lightningenable.com/dashboard/signup) - Then follow [Monetize Your API in 10 Minutes](/products/agentic-commerce/api-monetization) to gate a route and take your first payment. ## L402 in one breath L402 is the protocol underneath both sides. Payment *becomes* authorization: ``` 1. Agent requests a resource → GET /api/premium/data 2. Server answers 402 + invoice → HTTP 402 Payment Required WWW-Authenticate: L402 macaroon="…", invoice="lnbc…" 3. Agent pays the Lightning invoice → (~1 second, obtains the preimage) 4. Agent retries with the token → Authorization: L402 : 5. Server verifies & serves → HTTP 200 OK ``` The proof is cryptographic: the server checks that `SHA256(preimage) == payment_hash`. No accounts, no sign-up, no card on file — just a tiny payment and a receipt. Deeper dive: [How It Works](/products/agentic-commerce/how-it-works) and the [L402 Producer API](/products/agentic-commerce/l402-producer-api). ## Three ways to start You don't have to reach for a credit card to begin. There are three on-ramps — pick the one that fits: | On-ramp | Cost | What you get | Start | |---------|------|--------------|-------| | **Free Producer Sandbox** | Free — no card | 3 endpoints · 200 challenges/mo · 1,000 sats max per challenge | Email + confirmation link (open it, then type your email to confirm) at [dashboard/signup](https://api.lightningenable.com/dashboard/signup) | | **L402 Fast Lane** | 100 sats — no card, no form | A full **30-day Agentic Commerce** trial | Pay the challenge — [Activate with Lightning](/getting-started/activate-with-lightning) | | **Stripe Trial** | Card — no charge for 30 days | A full **30-day Agentic Commerce** trial | [Start the card trial](https://api.lightningenable.com/Checkout?plan=individual) | **L402 Fast Lane** is "the signup form that IS the protocol": you `POST` to the signup endpoint, get a `402`, pay a 100-sat Lightning challenge, and retry with the macaroon proof to activate the account — the same L402 flow your own users and agents will use. See [Activate with Lightning](/getting-started/activate-with-lightning) for the MCP, dev-CLI, and raw-protocol paths. ## Plans Every plan is a flat monthly subscription — **no per-transaction fees from us**. Free is the risk-free way to start; Agentic Commerce is the self-serve paid plan; teams that want white-glove onboarding and direct founder access can contact us about Agentic Commerce — Business. | Plan | Price | Highlights | |------|-------|------------| | **Free Producer Sandbox** | $0 | Prove L402 works with no card: 3 endpoints, 200 challenges/mo, 1,000 sats max per challenge | | **Agentic Commerce** | $49/mo | Unlimited L402 endpoints, Strike settlement, per-endpoint pricing, live dashboard, 30-day trial | | **Agentic Commerce — Business** | [Contact us](mailto:support@lightningenable.com) | Everything in Agentic Commerce **+** white-glove onboarding and direct founder access | You can pay for Agentic Commerce by card ($49/month, with a 30-day trial) or in sats over L402, 30 days per payment at the price the renewal quote (`GET /api/merchant/subscription/renew`) returns. See [Renew in sats](/getting-started/activate-with-lightning#renew-in-sats). See the [full plan comparison](/products/product-overview) for the feature-by-feature breakdown, and the [Agentic Commerce overview](/products/agentic-commerce/overview) for the product itself. :::note Using the MCP is free Only *creating* L402-protected endpoints needs a plan (or the Free Producer Sandbox). **Using** the MCP server as an agent to pay and access L402 APIs is free and needs no subscription. ::: ## Platform integrations (included with any paid plan) Already run a storefront? Lightning Enable drops L402 checkout into commerce platforms so agents (and bitcoin-aligned customers) can buy in sats: - **[Shopify Commerce](/products/shopify-commerce/overview)** — L402 agentic commerce for Shopify stores. ## Where to go next - **Building an agent?** [MCP Quick Start](/products/agentic-commerce/mcp-quickstart) → [Wallet Setup](/products/agentic-commerce/mcp-wallet-setup) → [MCP Complete Guide](/products/agentic-commerce/mcp-complete-guide) - **Monetizing an API?** [Monetize Your API in 10 Minutes](/products/agentic-commerce/api-monetization) → [L402 Producer API](/products/agentic-commerce/l402-producer-api) → [Dashboard Guide](/products/agentic-commerce/dashboard-guide) - **Just exploring?** [How It Works](/products/agentic-commerce/how-it-works) · [Ecosystem Map](/getting-started/ecosystem) · [Prerequisites](/getting-started/prerequisites) - **Setting up a provider?** [Strike (recommended)](/strike-setup/account-setup) or [OpenNode](/opennode-setup/account-setup) - **Accepting Bitcoin at a checkout instead of gating an API?** The e-commerce [Quick Start](/getting-started/quick-start) covers the invoice → QR → confirmation flow. ## Getting help - **Documentation:** you're reading it. - **Email:** support@lightningenable.com - **Strike support:** [strike.me](https://strike.me) - **OpenNode support:** [developers.opennode.com](https://developers.opennode.com) --- *Lightning Enable is a product of [Refined Element, LLC](https://refinedelement.com). It is API middleware and never holds your funds — your chosen settlement provider (Strike or OpenNode) facilitates custody and settlement, and every payment settles directly into your own provider account.* ============================================================================== # Prerequisites Source: https://docs.lightningenable.com/getting-started/prerequisites ============================================================================== # Prerequisites Before you begin integrating Lightning Enable, ensure you have the following ready. :::caution Provider Verification Required You **must** complete verification with your chosen payment provider (Strike or OpenNode) before you can accept payments. This is a regulatory requirement and cannot be bypassed. ::: ## Required ### 1. Payment Provider Account You need an account with at least one payment provider. **Strike is the default and recommended for most merchants.** To receive into your own Lightning wallet instead, see [Nostr Wallet Connect](/nwc-setup/account-setup) (receive-only; no refunds or on-chain). The hosted platform connects to provider **production** APIs, so you need a production key; sandbox and testnet keys don't work. Test with a demo invoice (`amount: 0`) or a small real payment. #### Option A: Strike (Recommended) **Steps:** 1. Sign up at [dashboard.strike.me](https://dashboard.strike.me) 2. Complete KYC verification 3. Generate an API key from your dashboard 4. Store the API key securely [Detailed Strike Setup Guide](/strike-setup/account-setup) #### Option B: OpenNode **Steps:** 1. Sign up at [app.opennode.com](https://app.opennode.com) 2. **Complete KYB verification** (required for mainnet, plan 2-4 business days) 3. Generate an API key 4. Store the API key securely [Detailed OpenNode Setup Guide](/opennode-setup/account-setup) ### 2. Lightning Enable Account You need a Lightning Enable account for your **merchant API key** — the key that lets you *create* L402-protected endpoints. Both paid plans include the full L402 stack (unlimited endpoints, per-endpoint pricing, the Producer API); the difference is settlement-provider choice and support, not whether you can monetize APIs. **Available Plans:** | Plan | Monthly | Best For | |------|---------|----------| | Free Producer Sandbox | Free — no card | Hobbyists, demos, and side projects (3 endpoints, 200 challenges/mo, 1,000 sats max per challenge) | | Agentic Commerce | $49 | Developers with real traffic (unlimited L402 endpoints, Strike settlement) | | Agentic Commerce — Business | Contact us | Teams and platforms — white-glove onboarding and direct founder access | Agentic Commerce and Agentic Commerce — Business both include access to all platform integrations (Shopify Commerce). Agentic Commerce also includes a **30-day free trial** through self-serve checkout; Business is contact-only (email support@lightningenable.com — it isn't purchasable through `/Checkout`), and any trial terms are arranged directly. :::tip You don't need a card to start Get a merchant API key for free with the **Free Producer Sandbox** ([sign up](https://api.lightningenable.com/dashboard/signup) — 3 endpoints, no card), or unlock a full 30-day Agentic Commerce trial by paying a 100-sat Lightning challenge — no card, no form — via [**L402 Fast Lane**](/getting-started/activate-with-lightning). ::: Every on-ramp gives you a merchant API key and dashboard access. ### 3. Development Environment Any platform that can make HTTP REST API calls: **Recommended:** - .NET 9 / ASP.NET Core - Node.js / TypeScript - Python - Any language with HTTP client support **Technical Requirements:** - HTTPS support (required for webhooks) - Ability to store API keys securely - JSON parsing capability ## Recommended ### Webhook Endpoint For production use, webhooks provide instant payment notifications: - **Public HTTPS URL** (not localhost) - **Ability to validate HMAC signatures** - **Idempotent processing** (webhooks may retry) For local development, use [ngrok](https://ngrok.com) to expose your local server. ## Platform-Specific Requirements ### For Agentic Commerce If you're implementing L402 API monetization: - **Production provider key** (mainnet/production payments required) - **Public API endpoint** to protect - **Strike recommended** for L402. OpenNode doesn't surface payment preimages, so it's a poor fit for L402 flows. ## Environment Checklist Use this checklist before starting integration: ### Development - [ ] Payment provider account created (Strike or OpenNode) - [ ] Provider production API key generated - [ ] Lightning Enable account created (Free Producer Sandbox, trial, or paid plan) - [ ] Merchant API key received - [ ] Development environment ready - [ ] ngrok installed (for webhook testing) - [ ] Flow tested with a demo invoice (`amount: 0`) ### Production - [ ] **Provider verification completed** (Strike KYC or OpenNode KYB) - [ ] Small real payment completed end to end - [ ] Webhook endpoint deployed and accessible - [ ] HTTPS configured - [ ] API keys stored securely (not in code) - [ ] Error logging and monitoring enabled ## Next Steps Once you have all prerequisites: 1. [Quick Start Guide](/getting-started/quick-start) - Create your first payment 2. Provider Setup — [Strike (recommended)](/strike-setup/account-setup) or [OpenNode](/opennode-setup/account-setup) 3. Choose your product: - [Agentic Commerce](/products/agentic-commerce/overview) - [Shopify Commerce](/products/shopify-commerce/overview) ============================================================================== # Quick Start Source: https://docs.lightningenable.com/getting-started/quick-start ============================================================================== # Quick Start Guide Get up and running with Lightning Enable in under 10 minutes. This guide walks you through creating your first Lightning Network payment for an **e-commerce / checkout** integration (invoice → QR → confirmation flow). :::info Building an L402-paid API instead? If your goal is to **monetize an API for AI agents** (pay-per-request gating, not checkout) you want the L402 walkthrough, not this page: [**Monetize Your API in 10 Minutes**](/products/agentic-commerce/api-monetization). The two flows use different endpoints. This page covers checkout; L402 is the API-monetization path. ::: :::tip Don't have an API key yet? [Start the Free Producer Sandbox](https://api.lightningenable.com/dashboard/signup) (no card) or see the [plans](/products/product-overview) to get your merchant API key. ::: :::caution Provider Verification Required Before accepting real payments, you must complete verification with your payment provider (Strike or OpenNode). ::: ## Prerequisites Before you begin, ensure you have: 1. **Payment Provider Account** with a **production** API key (the hosted platform connects to provider production APIs; sandbox and testnet keys don't work) - **Strike** (recommended): [dashboard.strike.me](https://dashboard.strike.me) - **OpenNode**: [app.opennode.com](https://app.opennode.com) 2. **Lightning Enable account** with your merchant API key 3. **HTTP Client** - curl, Postman, or any programming language ## Step 1: Test Authentication Verify your API key by calling an authenticated endpoint: ```bash curl -X GET https://api.lightningenable.com/api/merchant/me \ -H "X-API-Key: your-api-key-here" ``` ## Step 2: Configure Your Payment Provider Lightning Enable creates invoices with your own provider credential. Save it in the dashboard under **Settings**, or with the merchant settings API: ```bash # Configure Strike (recommended) — saving the key also defaults you to the Strike provider curl -X PUT https://api.lightningenable.com/api/merchant/strike-key \ -H "X-API-Key: your-merchant-api-key" \ -H "Content-Type: application/json" \ -d '{"strikeApiKey": "your-strike-api-key"}' # Or configure OpenNode curl -X PUT https://api.lightningenable.com/api/merchant/opennode-key \ -H "X-API-Key: your-merchant-api-key" \ -H "Content-Type: application/json" \ -d '{"openNodeApiKey": "your-opennode-api-key"}' # Set the active provider explicitly: "strike", "opennode", or "nwc" curl -X PUT https://api.lightningenable.com/api/merchant/payment-provider \ -H "X-API-Key: your-merchant-api-key" \ -H "Content-Type: application/json" \ -d '{"provider": "strike"}' # Optional: register a callback URL for webhook notifications curl -X PUT https://api.lightningenable.com/api/merchant/webhook-url \ -H "X-API-Key: your-merchant-api-key" \ -H "Content-Type: application/json" \ -d '{"webhookUrl": "https://yourapp.com/webhooks/lightning", "webhookSecret": "your-signing-secret"}' ``` ## Step 3: Create Your First Payment Create a Lightning invoice for a customer purchase: ```bash curl -X POST https://api.lightningenable.com/api/payments \ -H "X-API-Key: your-api-key-here" \ -H "Content-Type: application/json" \ -d '{ "orderId": "ORDER-12345", "amount": 25.00, "currency": "USD", "description": "Premium Widget", "customerEmail": "customer@example.com", "successUrl": "https://yourapp.com/order/success" }' ``` **Response:** ```json { "invoiceId": "1042", "providerChargeId": "charge_xyz789", "status": "unpaid", "amount": 25.00, "currency": "USD", "lightningInvoice": "lnbc250000n1p...", "onchainAddress": "bc1q...", "hostedCheckoutUrl": "https://checkout.opennode.com/charge_xyz789", // OpenNode only "payUrl": "https://api.lightningenable.com/pay/AbC...xyz", "createdAt": "2026-07-03T12:00:00Z", "expiresAt": "2026-07-03T13:00:00Z" } ``` :::note Invoice expiry varies by provider Always read the `expiresAt` field rather than assuming a fixed window: it's the deadline encoded in the BOLT11 invoice. Without `expiresInSeconds`, Strike invoices (the recommended default) live about 24 hours, OpenNode uses its default TTL, and demo invoices (`amount: 0`) expire in 10 minutes. Send `expiresInSeconds` (60 to 86400) to set the lifetime yourself; see [Invoice Expiration](/api-reference/payments#invoice-expiration). ::: ## Step 4: Display Payment Options You have three options for accepting payment: ### Option A: Hosted Checkout Redirect the customer to `payUrl`, the Lightning Enable hosted payment page for this invoice (every provider). Treat it as a payment link: anyone holding it can see the invoice. On OpenNode, `hostedCheckoutUrl` is OpenNode's own page. ```javascript window.location.href = response.payUrl; ``` ### Option B: Lightning Invoice (QR Code) Display the Lightning invoice as a QR code: ```html
``` ### Option C: On-Chain Bitcoin Address Display the Bitcoin address for on-chain payment (slower, higher fees): ```html

Send Bitcoin to: bc1q...

``` ## Step 5: Check Payment Status Poll the payment status endpoint: ```bash curl -X GET https://api.lightningenable.com/api/payments/1042 \ -H "X-API-Key: your-api-key-here" ``` **Response (Unpaid):** ```json { "invoiceId": "1042", "orderId": "ORDER-12345", "status": "unpaid", "amount": 25.00, "currency": "USD", "paidAt": null } ``` **Response (Paid):** ```json { "invoiceId": "1042", "orderId": "ORDER-12345", "status": "paid", "amount": 25.00, "currency": "USD", "paidAt": "2026-07-03T12:03:45Z" } ``` ## Step 6: Handle Webhooks (Recommended) Instead of polling, configure webhooks for instant payment notifications: **Webhook Payload:** ```json { "invoiceId": "1042", "orderId": "ORDER-12345", "status": "paid", "amount": 25.00, "currency": "USD", "paidAt": "2026-07-03T12:03:45Z" } ``` **Verify Webhook Signature:** The `X-LightningEnable-Signature` header has the format `t={unix_timestamp},v1={hmac_sha256_hex}`. Compute HMAC-SHA256 of `{timestamp}.{payload}` using your webhook secret. ```csharp using System.Security.Cryptography; using System.Text; public bool VerifyWebhookSignature(HttpRequest request, string payload, string secret) { var header = request.Headers["X-LightningEnable-Signature"].ToString(); // Parse t={timestamp},v1={hmac} format var parts = header.Split(',') .Select(p => p.Split('=', 2)) .ToDictionary(p => p[0], p => p[1]); var timestamp = parts["t"]; var receivedHmac = parts["v1"]; // Sign "{timestamp}.{payload}" var signingInput = $"{timestamp}.{payload}"; using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret)); var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(signingInput)); var computedHmac = Convert.ToHexString(hash).ToLowerInvariant(); // Constant-time comparison to avoid leaking the signature under timing analysis return CryptographicOperations.FixedTimeEquals( Encoding.UTF8.GetBytes(computedHmac), Encoding.UTF8.GetBytes(receivedHmac)); } ``` ## Payment Status Flow ``` +---------+ Customer pays +------------+ Confirmed +------+ | unpaid |-------------------->| processing |---------------->| paid | +---------+ +------------+ +------+ | | expiry (provider-dependent — see expiresAt) v +---------+ | expired | +---------+ ``` **Status Codes:** | Status | Description | |--------|-------------| | `unpaid` | Invoice created, awaiting payment | | `processing` | Payment detected, confirming | | `paid` | Payment confirmed, fulfill order | | `expired` | Invoice expired without payment | | `underpaid` | Insufficient amount received | | `refunded` | Payment was refunded | ## Complete Example: Node.js ```javascript const axios = require('axios'); const API_URL = 'https://api.lightningenable.com'; const API_KEY = 'your-api-key-here'; async function createPayment() { // Create payment const response = await axios.post( `${API_URL}/api/payments`, { orderId: `ORDER-${Date.now()}`, amount: 25.00, currency: 'USD', description: 'Premium Widget', successUrl: 'https://yourapp.com/order/success' }, { headers: { 'X-API-Key': API_KEY, 'Content-Type': 'application/json' } } ); console.log('Lightning Invoice:', response.data.lightningInvoice); // hostedCheckoutUrl is only present when using OpenNode as provider if (response.data.hostedCheckoutUrl) { console.log('Hosted Checkout URL:', response.data.hostedCheckoutUrl); } return response.data; } createPayment(); ``` ## Complete Example: C# ```csharp using System.Net.Http; using System.Net.Http.Json; public class LightningEnableClient { private readonly HttpClient _httpClient; private const string ApiUrl = "https://api.lightningenable.com"; public LightningEnableClient(string apiKey) { _httpClient = new HttpClient(); _httpClient.DefaultRequestHeaders.Add("X-API-Key", apiKey); } public async Task CreatePaymentAsync( string orderId, decimal amount, string description) { var request = new { orderId, amount, currency = "USD", description }; var response = await _httpClient.PostAsJsonAsync( $"{ApiUrl}/api/payments", request ); response.EnsureSuccessStatusCode(); return await response.Content.ReadFromJsonAsync(); } } ``` ## Troubleshooting **Problem:** `401 Unauthorized` response **Solution:** Check that your API key is correct and included in the `X-API-Key` header. --- **Problem:** `400` with `"error": "payment_provider_not_configured"` **Solution:** Save your payment provider credential (Step 2). The response's `docs` link names the setup guide for your provider. --- **Problem:** Payment stuck in `unpaid` status **Solution:** - Check the invoice hasn't expired — read the `expiresAt` field (expiry is provider-dependent; see the note in Step 3) - Verify the Lightning invoice or Bitcoin address - To test the flow without paying, create a demo invoice (`amount: 0`); it auto-confirms. Otherwise test with a small real payment (about $1) ## Next Steps - [First Payment Tutorial](/getting-started/first-payment) - Detailed walkthrough - [Webhooks Setup](/api-reference/webhooks) - Stop polling, use webhooks - Provider Configuration — [Strike (recommended)](/strike-setup/account-setup) or [OpenNode](/opennode-setup/account-setup) ## Stuck? Start with [Troubleshooting](/troubleshooting) for the common failures and their fixes. If something's broken or not working, email support@lightningenable.com — a real person reads it. ============================================================================== # Sell With Your Agent Source: https://docs.lightningenable.com/getting-started/sell-with-your-agent ============================================================================== # Sell With Your Agent: Zero to a Paid API You already activated a Lightning Enable account (the [previous step](./activate-with-lightning)). This guide is the next one: turn that account into a **paid, agent-discoverable endpoint** without touching the dashboard or writing any integration code yourself. The [`producer-setup` skill](https://github.com/refined-element/lightning-enable-skills/tree/main/skills/producer-setup) drives the whole thing through the Lightning Enable MCP server. You give your agent three things and one sentence; it does the rest — configuring your receive lane, wrapping your API, publishing a machine-readable manifest, and proving the whole path works with a real, tiny Lightning payment before it tells you it's done. :::tip Works wherever the MCP server runs Claude Code, Claude Desktop, or any MCP-compatible agent host with the [Lightning Enable MCP](/products/agentic-commerce/mcp-quickstart) connected and a merchant API key set (`LIGHTNING_ENABLE_API_KEY`). If you don't have a key yet, [activate one for free with a ~100-sat L402 payment](./activate-with-lightning) — no card. `setup_wallet` and `l402_producer` ship in the default `standard` [tool profile](/products/agentic-commerce/mcp-complete-guide#tool-profiles) — nothing extra to enable. ::: ## What you provide Three things, and the agent needs no more from you than this: | You provide | Why | |---|---| | **A wallet to receive payments** — a Nostr Wallet Connect (NWC) connection string from a wallet you already run (CoinOS, Alby Hub, or another NIP-04 wallet), *or* a Strike/OpenNode API key | This is where the sats your endpoint earns land. Lightning Enable never touches it beyond asking for an invoice and asking later whether it was paid. | | **A spend ceiling for the agent's own wallet** — a dollar or sat cap, e.g. "$5 max" | The agent pays a tiny self-test challenge against your new endpoint before handing it back to you, so it needs a small amount of its own spending room. This is the same budget mechanism documented in [AI Spending Security](/products/agentic-commerce/ai-spending-security) — the wallet balance and the config-file caps are the real ceiling, not the agent's judgment. | | **The API to monetize, and a price** — a base URL and a sats-per-call figure | What gets wrapped and what it costs. Any HTTPS API works; see [Setting Up Your Proxy](/products/agentic-commerce/proxy-setup-walkthrough) for the constraints (no raw IPs, no internal hostnames, standard ports only). | ## The one sentence This is the literal instruction — say it to your agent once the MCP server is connected: > Set me up to sell my API over Lightning: use the wallet you already have connected to receive payments, keep the signup and self-test under 200 sats, charge 25 sats per call on `https://api.mycompany.com/v1/forecast`, publish it, and show me the OpenAPI URL and the challenge feed when you are done. Swap in your own URL and price. To receive with Strike or OpenNode instead of NWC, say "using my Strike API key"; the skill accepts either. ## What the skill does, step by step The skill calls the Lightning Enable MCP tools in this order. Nothing here requires you to open the dashboard. 1. **`setup_wallet`** — if the agent doesn't already have a paying wallet configured, this writes `~/.lightning-enable/config.json` with a small wallet and the spend ceiling you gave it. This is the wallet the agent uses in step 7 to pay its own self-test challenge — it is separate from the receive-side wallet in the next step. 2. **`l402_producer(action="configure_receive", ...)`** — saves your receive-side credential and switches your account onto that lane. For NWC this wraps `PUT /api/merchant/nwc-connection` followed by `PUT /api/merchant/payment-provider`; for Strike/OpenNode it wraps the equivalent key endpoint. See [Nostr Wallet Connect setup](../nwc-setup/account-setup.md) for what this credential looks like and what it can and can't do. 3. **`l402_producer(action="status")`** — confirms the receive lane actually took (provider set, credential present, reachable) before anything downstream depends on it. Cheap, read-only, and worth insisting the agent check before it moves on. 4. **`l402_producer(action="create_proxy", ...)`** — wraps your API in an L402-gated proxy pointed at the target URL and default price (`POST /api/proxy`). 5. **`l402_producer(action="add_endpoint", ...)`** — registers the specific path, method, summary, and price you asked for so it's visible in the manifest with its own description, and, when it safely can, makes that price what the proxy charges for the path (otherwise it returns a note explaining why it didn't). 6. **`l402_producer(action="publish", ...)`** — turns on the manifest and (if you asked for discoverability) lists it in the public L402 registry. Public listing needs a proxy whose ownership is verified; until then the listing request answers `422 ownership_verification_required` and everything else still works by direct URL. See [Verify ownership to list publicly](#verify-ownership-to-list-publicly). 7. **`l402_producer(action="create", ...)`** then a payment from the wallet configured in step 1 — the skill mints a one-off, tiny challenge against your brand-new endpoint and pays it itself, inside the spend ceiling you set. 8. **`l402_producer(action="verify", ...)`** — verifies its own payment. This is the same cryptographic check any real buyer's payment goes through: `SHA256(preimage) == payment_hash`, macaroon signature valid, not expired. A `valid: true` here means a stranger paying the same way would also get in. 9. **`l402_producer(action="list_challenges")`** — pulls the challenge feed back so you can see the self-test entry as a receipt, not just take the agent's word for it. If any step fails, the skill stops and reports which one — it does not silently retry into a different configuration than the one you asked for. ## What you get back At the end of a successful run, the agent hands you four things: - **An OpenAPI document** at `/l402/proxy/{proxyId}/openapi.json` — every visible endpoint carries an `x-payment` extension (price in sats, supported payment protocols, token validity window) and a `402` response pointing at the L402 challenge shape. Point any OpenAPI-aware tool at this URL directly. - **A manifest** at `/l402/proxy/{proxyId}/.well-known/l402-manifest.json` — the L402-native discovery document other agents read to learn what your API does and what it costs. Both documents describe the same endpoints from the same pricing logic; they can't disagree. - **A challenge listing** — `GET /api/l402/challenges?status=paid` shows every payment your endpoint has collected, newest first, with `paymentHash`, `resource`, `amountSats`, and `paidAt`. This is your feed, not a settlement record — see the next section. - **A `l402.challenge.paid` webhook**, once you point `CallbackUrl` at your own server (the skill can set this too, if you gave it a URL — otherwise set it yourself with `PUT /api/merchant/webhook-url`). Fires the first time each challenge is proven paid, HMAC-signed the same way as every other Lightning Enable webhook. ## Verify ownership to list publicly Your endpoint works for paying callers as soon as it's created. To list it in the public registry, agent discovery, and Nostr, you first prove that you control the API behind it. You publish a token in one of three places, then ask Lightning Enable to check: | Method | Where the token goes | Choose it when | |---|---|---| | Well-known file | `https://{target host}/.well-known/lightning-enable.json` containing `{"verify":""}` | You can add a file at the root of the API's host. | | DNS TXT record | `_lightning-enable.{target host}` with the token as its value | Someone else hosts the API, but you manage DNS for its hostname. | | Response header | `X-Lightning-Enable-Verify: ` on the target's base URL | You own the API's code but not its web root or DNS. | 1. Get a token with `POST /api/proxy/{proxyId}/verification/token`. It's shown once. If you created the proxy with the one-payment claim, the claim response already carries it in `verification.token`, along with `verification.instructions` (the exact file URL, record name, and header for your proxy) and `verification.verifyEndpoint`. 2. Publish the token by one method. 3. Run the check with `POST /api/proxy/{proxyId}/verify`. A response with `verified: true` means you can turn on listing; otherwise `reason` says what each method found. Lightning Enable re-checks verified proxies daily. Leave the token in place: three failed re-checks in a row make the proxy unverified and unlist it, though it keeps working by direct URL. The full walkthrough, with copy-paste examples and troubleshooting, is [Verify ownership of your API](/products/agentic-commerce/verify-ownership). ## Custody and fees Lightning Enable does not hold funds. Every sat your endpoint earns settles directly with your wallet — your own NWC wallet facilitates custody and settlement on that lane, or your chosen payment provider (Strike or OpenNode) does if you configured one of those instead. Lightning Enable never sits in the money's path; it mints the invoice, verifies the proof of payment, and gets out of the way. There is no per-transaction fee from Lightning Enable on any plan. Your wallet or payment provider may apply its own routing or processing fee — check their fee schedule, not ours. Selling requires a producer account: the Free Producer Sandbox, a 30-day Agentic Commerce trial ([Fast Lane](./activate-with-lightning) or card), or a paid plan. ## Trial, then the Free Producer Sandbox If you activated with the Fast Lane, you're on a 30-day **Agentic Commerce** trial: unlimited endpoints, no volume cap on challenges, no per-challenge price ceiling. Everything above works at any scale during the trial. When the trial ends without billing added, your account automatically moves to the **Free Producer Sandbox** — you keep the account and every endpoint you built, but three caps now apply: **3 endpoints** (distinct resource paths, counted for the life of the account, never resets), **200 challenges/month**, and **1,000 sats max per challenge**. Add billing (Agentic Commerce, $49/month) at any point to remove the caps without losing anything you've built. Nothing above changes if you start directly on the Sandbox instead of the trial — the same skill and the same tool calls work; `add_endpoint` and `create` simply start refusing once you hit a cap, with a `402` naming which one. ## Renew If you pay for your plan in sats, nothing renews automatically. Before your paid-through date passes, pay the next period with the same L402 flow you used to sign up or claim. A card subscription renews through billing instead; see [Who can renew in sats](./activate-with-lightning#who-can-renew-in-sats). 1. **Read the quote.** `GET /api/merchant/subscription/renew` returns `priceSats`, `periodDays`, whether you can renew (`eligible`), and the paid-through date a payment would produce (`periodEndIfPaid`). It creates no invoice. If you created your account through the claim endpoint (`POST /api/producer/claim`), the price counts your active proxies and the `breakdown` block shows the calculation; see [How producer pricing works](/products/agentic-commerce/producer-pricing). Set your agent's spending limit from `priceSats`, with a small margin for the exchange rate, never from a figure you copied earlier. The paid request converts at the live rate, so the 402's invoice amount, which is what you pay, can be a little above the quote. 2. **Pay the `402`.** `POST /api/merchant/subscription/renew` with your API key and no body answers `402` with an invoice and a macaroon. Pay the invoice, then send the same request again, still with no body, adding `Authorization: L402 :`. If you created your account through the claim endpoint, the challenge is honored at the sats amount it was issued for, even if the price changes before you pay. For any other account, a challenge minted before a price increase is refused; request a fresh one. 3. **Check the new date.** The `200` response carries your new paid-through date in `currentPeriodEnd`. For an account created through the claim endpoint, the same date also shows up as `paidThrough` in `GET /api/producer/claim/status` and as `paid_through` in each proxy's manifest. To get a reminder, set a callback URL (step 7 of the REST walkthrough below). Lightning Enable sends `l402.subscription.expiring` about seven days and about one day before your paid-through date, with the current quote in `quoteSats`; see [Subscription expiring webhook](/products/agentic-commerce/producer-api-reference#subscription-expiring-webhook). The MCP tool `renew_lightning_enable_subscription` and other clients are covered in [Renew in sats](./activate-with-lightning#renew-in-sats). ## Troubleshooting **"The wallet did not answer within 30s."** This is an NWC timeout, and it can surface at two different points: during `configure_receive` (your receive wallet didn't respond to the connection check) or during the agent's own self-test payment in step 7 (its paying wallet, if also NWC, didn't respond). Check the connection is still listed in the wallet app and that it grants `make_invoice` and `lookup_invoice`; see [Nostr Wallet Connect setup — Troubleshooting](../nwc-setup/account-setup.md#troubleshooting) for the full list of NWC failure modes. **The self-test payment fails when the receiving and paying wallets are the same node.** If you pointed both `configure_receive` and the agent's own wallet (`setup_wallet`) at the *same* underlying Lightning node or NWC wallet, the self-test payment in step 7 can fail — most Lightning implementations refuse to route a payment back to the node that issued the invoice, since there's no real path to route through. This isn't a Lightning Enable restriction; it's how Lightning payment routing works. Use two different wallets — the receive side can stay whatever you chose, but give the agent a second, separate small wallet to pay from. This is exactly the setup already recommended in [Run L402 Anywhere: Hermes + NWC](/products/agentic-commerce/hermes-nwc-setup) and [First success after activation](./activate-with-lightning#first-success-after-activation) — a dedicated small paying wallet, kept separate from anything that receives. **500 "Payment configuration error" on the first 402.** This is different from the clean `400 payment_provider_not_configured` refusal (see the [Producer API Reference](/products/agentic-commerce/producer-api-reference#error-format)), which means no credential at all is on file. A `500` with "Payment configuration error" means a credential **is** on file but isn't usable — confirmed causes: a placeholder/invalid API key saved for Strike or OpenNode, or an NWC wallet that never answers the invoice request (`NWC_NO_RESPONSE`, including a wallet that's offline or requires an encryption scheme Lightning Enable doesn't support — see [Choose a wallet](../nwc-setup/account-setup.md#choose-a-wallet)). Fix it with `configure_receive` (re-save a real credential) and, if the agent's own wallet is the one that's unusable, `setup_wallet` — then retry. Run `l402_producer(action="status")` (or `GET /api/merchant/quickstart` by hand) afterward to confirm the lane is actually live before minting again. ## Do it by hand (raw REST) Everything above is the same handful of HTTP calls the skill makes for you. Useful if you're integrating from a language without the skill, or want to see exactly what's happening. All calls need `X-API-Key: $LIGHTNING_ENABLE_API_KEY`. **1. Configure the receive lane (NWC shown; swap for `strike-key`/`opennode-key` if using a provider):** ```bash curl -X PUT https://api.lightningenable.com/api/merchant/nwc-connection \ -H "X-API-Key: $LIGHTNING_ENABLE_API_KEY" -H "Content-Type: application/json" \ -d '{"nwcConnectionString": "nostr+walletconnect://?relay=wss://&secret="}' curl -X PUT https://api.lightningenable.com/api/merchant/payment-provider \ -H "X-API-Key: $LIGHTNING_ENABLE_API_KEY" -H "Content-Type: application/json" \ -d '{"provider": "nwc"}' ``` **2. Check status:** ```bash curl https://api.lightningenable.com/api/merchant/quickstart \ -H "X-API-Key: $LIGHTNING_ENABLE_API_KEY" ``` **3. Create the proxy:** ```bash curl -X POST https://api.lightningenable.com/api/proxy \ -H "X-API-Key: $LIGHTNING_ENABLE_API_KEY" -H "Content-Type: application/json" \ -d '{ "name": "Forecast API", "targetBaseUrl": "https://api.mycompany.com", "defaultPriceSats": 25, "description": "7-day forecast, priced per call" }' ``` Note the `proxyId` in the response (e.g. `forecast-api-a1b2`) — every call below uses it. **4. Add the endpoint to the manifest:** ```bash curl -X POST https://api.lightningenable.com/api/proxy/forecast-api-a1b2/manifest/endpoints \ -H "X-API-Key: $LIGHTNING_ENABLE_API_KEY" -H "Content-Type: application/json" \ -d '{ "endpointId": "forecast", "path": "/v1/forecast", "httpMethod": "GET", "summary": "7-day forecast", "basePriceSats": 25 }' ``` When `basePriceSats` differs from what the proxy would otherwise charge for this path, the call also writes a matching [pricing rule](/products/agentic-commerce/proxy-configuration) so `basePriceSats` becomes the charge. It skips the rule when the rule would reprice another endpoint, an existing rule takes precedence, or the path can't be expressed as one rule; the response then carries a `runtimePricingNote` saying why, and you set prices with `POST /api/proxy/{proxyId}/pricing`. A written wildcard rule returns `runtimePricingInfo`, because it also prices unlisted paths under its prefix. The public manifest always advertises what the proxy actually charges. **5. (To list publicly) Verify ownership.** Issue a token, publish it by [one of the three methods](/products/agentic-commerce/verify-ownership#verification-methods), then run the check: ```bash curl -X POST https://api.lightningenable.com/api/proxy/forecast-api-a1b2/verification/token \ -H "X-API-Key: $LIGHTNING_ENABLE_API_KEY" # Publish the returned token, for example as a DNS TXT record at # _lightning-enable.api.mycompany.com, then: curl -X POST https://api.lightningenable.com/api/proxy/forecast-api-a1b2/verify \ -H "X-API-Key: $LIGHTNING_ENABLE_API_KEY" ``` Skip this step if you don't want a public listing; set `manifestPubliclyListed` to `false` in the next step. **6. Publish (enable the manifest, optionally list in the registry):** ```bash curl -X PUT https://api.lightningenable.com/api/proxy/forecast-api-a1b2/manifest/settings \ -H "X-API-Key: $LIGHTNING_ENABLE_API_KEY" -H "Content-Type: application/json" \ -d '{ "manifestEnabled": true, "serviceDescription": "7-day weather forecasts, priced per call.", "manifestPubliclyListed": true }' ``` Listing an unverified proxy answers `422` with `"error": "ownership_verification_required"`. **7. (Optional) Point the payment webhook at your own server:** ```bash curl -X PUT https://api.lightningenable.com/api/merchant/webhook-url \ -H "X-API-Key: $LIGHTNING_ENABLE_API_KEY" -H "Content-Type: application/json" \ -d '{"webhookUrl": "https://your-server.example.com/webhooks/lightning-enable"}' ``` **8–9. Mint and pay a self-test challenge** (or ask a real caller to — the endpoint is live once step 6 completes): ```bash curl -X POST https://api.lightningenable.com/api/l402/challenges \ -H "X-API-Key: $LIGHTNING_ENABLE_API_KEY" -H "Content-Type: application/json" \ -H "Idempotency-Key: self-test-1" \ -d '{"resource": "/v1/forecast", "priceSats": 25, "description": "Self-test"}' ``` Pay the returned invoice with any Lightning wallet that surfaces a preimage. **10. Verify the payment:** ```bash curl -X POST https://api.lightningenable.com/api/l402/challenges/verify \ -H "X-API-Key: $LIGHTNING_ENABLE_API_KEY" -H "Content-Type: application/json" \ -d '{"macaroon": "", "preimage": "", "resource": "/v1/forecast"}' ``` **11. List challenges:** ```bash curl "https://api.lightningenable.com/api/l402/challenges?status=paid&limit=10" \ -H "X-API-Key: $LIGHTNING_ENABLE_API_KEY" ``` **12. Renew before your paid-through date:** ```bash # Read the quote: priceSats, periodDays, eligible, periodEndIfPaid curl https://api.lightningenable.com/api/merchant/subscription/renew \ -H "X-API-Key: $LIGHTNING_ENABLE_API_KEY" # Ask for the challenge (no body); the 402 carries the invoice and macaroon curl -i -X POST https://api.lightningenable.com/api/merchant/subscription/renew \ -H "X-API-Key: $LIGHTNING_ENABLE_API_KEY" # After paying the invoice, send the same request with the credential curl -X POST https://api.lightningenable.com/api/merchant/subscription/renew \ -H "X-API-Key: $LIGHTNING_ENABLE_API_KEY" \ -H "Authorization: L402 :" ``` Full contract for every call above — request/response shapes, error codes, idempotency rules, caveat enforcement — is in the [Producer API Reference](/products/agentic-commerce/producer-api-reference) and [Proxy Configuration](/products/agentic-commerce/proxy-configuration). ## Next steps - [L402 Producer API: Agents That Earn](/products/agentic-commerce/l402-producer-api) — the full producer guide, MCP tool parameters, and idempotency rules - [Producer API Reference](/products/agentic-commerce/producer-api-reference) — complete REST contract for every call above - [Verify ownership of your API](/products/agentic-commerce/verify-ownership) — prove you control the API so the proxy can be listed publicly - [Setting Up Your Proxy](/products/agentic-commerce/proxy-setup-walkthrough) — the manual dashboard walkthrough this guide automates, including the OpenAPI document details - [Nostr Wallet Connect setup](../nwc-setup/account-setup.md) — the receive lane in depth - [AI Spending Security](/products/agentic-commerce/ai-spending-security) — how the agent's own spend ceiling is enforced --- *Lightning Enable is API middleware and never holds your funds. Your wallet, or your chosen payment provider (Strike or OpenNode), facilitates custody and settlement of every payment your endpoint earns.* ============================================================================== # Nostr Wallet Connect Setup Source: https://docs.lightningenable.com/nwc-setup/account-setup ============================================================================== # Nostr Wallet Connect setup Nostr Wallet Connect (NWC, [NIP-47](https://github.com/nostr-protocol/nips/blob/master/47.md)) connects Lightning Enable to a Lightning wallet you already run. Lightning Enable asks your wallet for an invoice, and asks it again later whether that invoice was paid. There is no payment provider account in between. :::tip No payment-provider account, no onboarding This is the fastest lane to start receiving. If you have a wallet that speaks NWC, you can be minting L402 challenges in about a minute — no separate payment-provider signup or API key. ::: Lightning Enable does not hold funds on this lane. Your wallet facilitates custody and settlement; Lightning Enable speaks NIP-47 to it and nothing else. ## What you get, and what you don't NWC is connected as a **receive-only** lane. Lightning Enable never instructs your wallet to send. | Capability | NWC | Strike | OpenNode | |---|---|---|---| | Create Lightning invoices | Yes | Yes | Yes | | Returns payment preimage | Yes | Yes | No | | Payment detection | Polling, up to 60s | Webhook | Webhook | | On-chain addresses | No | Yes | Yes | | Fiat-denominated invoices | No | Yes | Yes | | Refunds through the API | No | Yes | Yes | Two absences are worth reading twice: - **No webhook.** NIP-47 defines none. Lightning Enable detects payment by asking your wallet (`lookup_invoice`) once a minute, so an invoice can take up to 60 seconds to show as paid. Your own `CallbackUrl` webhook still fires, with the same payload shape the other providers send and `"provider": "nwc"`. - **No refunds.** A refund is an outgoing payment, and this lane never sends. Refund a payer from your wallet app, or connect a [Strike account](../strike-setup/account-setup.md) if you need refunds issued through the API. ## Choose a wallet Lightning Enable supports both NIP-47 encryption schemes. Before the first request it reads your wallet's signed capability (INFO) event and uses NIP-44 v2 when the wallet offers it, otherwise NIP-04, the original default. Known to work: [CoinOS](https://coinos.io) (NIP-04) and [Alby](https://getalby.com). ## Create the connection 1. Open your wallet app and find its Nostr Wallet Connect (or "app connections") screen. 2. Create a new connection for Lightning Enable. 3. Grant it the `make_invoice` and `lookup_invoice` permissions. Lightning Enable calls nothing else, so do not grant `pay_invoice` — the connection cannot spend, and it should not be able to. 4. Set a budget of zero if your wallet offers one. A receive-only connection needs no spending allowance. 5. Copy the connection string. It looks like this: ``` nostr+walletconnect://?relay=wss://&secret= ``` :::warning Treat this string as a credential The `secret` in that string authorises NIP-47 calls against your wallet. Store it the way you would store an API key. Lightning Enable encrypts it at rest with AES-256-GCM and never returns it from any endpoint — the settings API answers `` and nothing more. ::: ## The fastest path: the MCP tool If you're driving setup through the Lightning Enable MCP server (for example, via the [`producer-setup` skill](/getting-started/sell-with-your-agent)), one tool call replaces the two REST calls below: ```text l402_producer(action="configure_receive", nwcConnectionString="nostr+walletconnect://...") ``` This saves the connection string and switches your account onto the NWC lane in one step. Follow it with `l402_producer(action="status")` to confirm the lane is live before you mint anything against it — see [Sell With Your Agent](/getting-started/sell-with-your-agent) for the full flow. ## Save it in Lightning Enable Prefer to do it by hand? Save the string with your merchant API key: ```bash curl -X PUT https://api.lightningenable.com/api/merchant/nwc-connection \ -H "X-API-Key: $LIGHTNING_ENABLE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "nwcConnectionString": "nostr+walletconnect://?relay=wss://&secret=" }' ``` ```json { "success": true, "message": "Nostr Wallet Connect connection saved. Lightning Enable will create invoices on your wallet and poll it for payment — no webhook is needed.", "nwcConnectionString": "" } ``` The connection string is validated before it is stored, so a malformed string comes back as a `400` instead of failing later on your first real payment. Saving a connection string also puts your account on the NWC lane if you had not chosen a provider yet. To switch lanes explicitly: ```bash curl -X PUT https://api.lightningenable.com/api/merchant/payment-provider \ -H "X-API-Key: $LIGHTNING_ENABLE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"provider": "nwc"}' ``` ## Check it works Mint a challenge. A successful mint proves the whole path: Lightning Enable reached your relay, your wallet decrypted the request, and it returned a real BOLT11 invoice. ```bash curl -X POST https://api.lightningenable.com/api/l402/challenges \ -H "X-API-Key: $LIGHTNING_ENABLE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"resource": "/hello", "priceSats": 10, "description": "NWC smoke test"}' ``` Pay the invoice from another wallet, then wait up to a minute. The invoice flips to `paid`, your `CallbackUrl` receives the webhook, and the preimage becomes available for L402 verification. ## How amounts work NWC settles in Bitcoin, and Lightning Enable performs no currency conversion on this lane. Request invoices in `BTC` (or `SAT` for whole satoshis). A fiat currency such as `USD` is refused and no invoice is created. L402 challenges are already priced in satoshis, so they need no change. Invoices are for whole satoshis, with a minimum of 1. ## Troubleshooting **"The wallet did not answer within 30s."** The wallet app is offline or the connection was revoked in the wallet. Check the connection is still listed in your wallet app, then re-copy and save the string. **"Could not reach any of the relays in the wallet's NWC connection string."** The relay host in the string is down or unreachable. Some wallets let you regenerate a connection against a different relay; a string that advertises two relays fails over automatically. **"The wallet's reply could not be decrypted."** The connection was rotated in the wallet app, so the secret Lightning Enable holds no longer matches. Create a fresh connection and save the new string. **A paid invoice still shows as unpaid.** Detection on this lane polls once a minute, so allow 60 seconds. Beyond that, confirm the connection still has the `lookup_invoice` permission — a connection granted only `make_invoice` can create invoices it can never confirm. ## See also - [Sell With Your Agent](/getting-started/sell-with-your-agent) — zero to a paid, agent-discoverable endpoint using this connection, driven end to end by an MCP agent - [Strike account setup](../strike-setup/account-setup.md) — the default lane, with webhooks, fiat invoices, and refunds - [L402 Producer API](../products/agentic-commerce/l402-producer-api.md) — minting and verifying paid access once your wallet is connected ## Stuck? Start with [Troubleshooting](/troubleshooting) for the common failures and their fixes. If something's broken or not working, email support@lightningenable.com — a real person reads it. ============================================================================== # Account Setup Source: https://docs.lightningenable.com/opennode-setup/account-setup ============================================================================== # OpenNode Account Setup :::warning Legacy provider OpenNode is a legacy per-merchant option. Strike is the default payment provider, and new accounts should start there. OpenNode does not surface payment preimages on outgoing payments, which makes it a poor fit for L402 work and means refunds behave differently. Receiving L402 payments does work on OpenNode. Unless you have a specific reason to stay, use [Strike](/strike-setup/account-setup). ::: OpenNode is a payment software provider that facilitates Bitcoin custody, settlement, and KYB compliance. Lightning Enable uses your OpenNode API key to create invoices and track their payment status. :::warning OpenNode KYB Required You must complete OpenNode's KYB (Know Your Business) verification before accepting live payments. This is required by regulation and cannot be bypassed. ::: ## Why OpenNode? OpenNode provides: - **Lightning Network support** - Instant Bitcoin payments - **Automatic conversion** - Convert to fiat if desired - **Bank settlements** - Withdraw to your bank account - **KYB/AML compliance** - Regulatory compliance handled for you Lightning Enable uses OpenNode as the underlying payment software provider, giving you: - API middleware architecture (Lightning Enable does not hold funds; OpenNode facilitates custody and settlement) - Bring Your Own API Key (BYOA) model - Full control over your payment settings ## Create OpenNode Account ### Step 1: Sign Up 1. Visit [app.opennode.com/signup](https://app.opennode.com/signup/) 2. Click **"Get Started"** or **"Sign Up"** 3. Enter your email and create a password 4. Verify your email address ### Step 2: Choose Account Type Select the appropriate account type: | Type | Best For | |------|----------| | **Individual** | Freelancers, solo developers | | **Business** | Companies, e-commerce platforms | | **Enterprise** | High-volume merchants, platforms | ### Step 3: Complete KYB Verification OpenNode requires business verification to comply with financial regulations. **Individual Accounts:** - Government-issued ID (passport, driver's license) - Proof of address (utility bill, bank statement) - Phone number verification **Business Accounts:** - Company registration documents - Proof of business address - Director/owner identification - Bank account verification :::info Verification Time KYB verification typically takes 2-4 business days. While waiting you can explore OpenNode's development/testnet environment directly — but note that Lightning Enable's hosted platform requires a **production** OpenNode key, so end-to-end testing starts once KYB completes. ::: ## Development vs Production OpenNode provides two environments: ### Development (Testnet) - **URL:** `dev-api.opennode.com` - **Dashboard:** `app.dev.opennode.com` - **Bitcoin:** Testnet (free test bitcoins) - **KYB:** Not required - **Use for:** Testing and development ### Production (Mainnet) - **URL:** `api.opennode.com` - **Dashboard:** `app.opennode.com` - **Bitcoin:** Real mainnet Bitcoin - **KYB:** Required - **Use for:** Live payments :::warning Hosted Platform Uses Production Only Lightning Enable's hosted platform connects to OpenNode's **production** API — development-environment keys cannot be used with it. The dev environment is useful for exploring OpenNode's own API on testnet, but to test your Lightning Enable integration, use a production key and a small real payment. See [Testing](/opennode-setup/testing). ::: ## Account Security ### Enable Two-Factor Authentication 1. Go to **Settings** > **Security** 2. Click **Enable 2FA** 3. Scan QR code with authenticator app 4. Enter verification code 5. Save backup codes securely ### API Key Security - Never share your API keys - Store any copy in a password manager or secret store — the key itself lives encrypted in Lightning Enable once you save it in the dashboard - Rotate keys periodically ### IP Allowlisting OpenNode lets you restrict API access by source IP (**Settings** > **Security**). Do **not** enable this for the key you use with Lightning Enable: API calls to OpenNode come from Lightning Enable's hosted platform, whose outbound IP addresses are not static, so an IP allowlist would intermittently break payments. ## Settlement Configuration ### Bitcoin Holdings Keep received payments in Bitcoin: 1. Go to **Settings** > **Settlement** 2. Select **"Keep in Bitcoin"** 3. Withdraw manually when desired ### Auto-Convert to Fiat Automatically convert to USD/EUR: 1. Go to **Settings** > **Settlement** 2. Select **"Convert to Fiat"** 3. Choose your currency 4. Set conversion percentage (0-100%) ### Bank Settlements Receive fiat to your bank account: 1. Complete business verification 2. Add bank account details 3. Set settlement frequency (daily, weekly) 4. Configure minimum settlement amount ## Troubleshooting ### Account Verification Pending If verification is taking longer than expected: 1. Check email for requests for additional documents 2. Ensure all documents are clear and readable 3. Contact OpenNode support ### Account Locked If your account is locked: 1. Check email for explanation 2. Respond to any compliance requests 3. Contact support@opennode.com ### Test Payments Not Working If test payments through Lightning Enable are failing: 1. Confirm your OpenNode key is a **production** key from [app.opennode.com](https://app.opennode.com) (dev keys don't work with the hosted platform) 2. Validate the key: `POST /api/merchant/validate-opennode` with your merchant API key 3. Check your OpenNode account is active and KYB-verified 4. See [Testing](/opennode-setup/testing) for the full walkthrough ## Next Steps Once your OpenNode account is set up: - [API Keys](/opennode-setup/api-keys) - Generate and configure API keys - [Webhooks](/opennode-setup/webhooks) - Set up payment notifications - [Testing](/opennode-setup/testing) - Test your integration ============================================================================== # API Keys Source: https://docs.lightningenable.com/opennode-setup/api-keys ============================================================================== # OpenNode API Keys :::warning Legacy provider OpenNode is a legacy per-merchant option. Strike is the default payment provider, and new accounts should start there. OpenNode does not surface payment preimages on outgoing payments, which makes it a poor fit for L402 work. Unless you have a specific reason to stay, see [Strike API Keys](/strike-setup/api-keys). ::: API keys authenticate your requests to OpenNode through Lightning Enable. This guide shows how to generate, configure, and secure your API keys. ## API Key Types OpenNode provides different API key types: | Key Type | Permissions | Use Case | |----------|-------------|----------| | **Invoice** | Create invoices, check status | Payment creation | | **Withdrawal** | Send payments, refunds | Refund processing | | **Admin** | Full account access | Account management | :::warning Lightning Enable requires an **Invoice** key for basic payments, or an **Admin** key if you need refund capabilities. ::: ## Generate API Key ### Development Environment :::note Dev-environment keys are useful for exploring OpenNode's own API on testnet, but they **cannot be used with Lightning Enable's hosted platform**, which connects to OpenNode production. Use a production key below when configuring Lightning Enable. ::: 1. Log in to [app.dev.opennode.com](https://app.dev.opennode.com) 2. Navigate to **Integrations** > **API Keys** 3. Click **Generate New Key** 4. Select key type: - **Invoice** for payment creation - **Admin** for full access (including refunds) 5. Copy and securely store the key ### Production Environment 1. Log in to [app.opennode.com](https://app.opennode.com) 2. Navigate to **Integrations** > **API Keys** 3. Click **Generate New Key** 4. Select key type 5. Copy and securely store the key :::danger API keys are shown only once. If you lose it, you'll need to generate a new one. ::: ## Configure in Lightning Enable ### Via Dashboard Sign in to your Lightning Enable dashboard and navigate to **Settings → Payment Provider** to paste your OpenNode API key. ### Via Merchant API ```bash curl -X PUT https://api.lightningenable.com/api/merchant/opennode-key \ -H "X-API-Key: your-merchant-api-key" \ -H "Content-Type: application/json" \ -d '{ "openNodeApiKey": "your-opennode-api-key" }' ``` :::note These are the only two configuration methods Your OpenNode key is stored by Lightning Enable's hosted platform, encrypted at rest with AES-256-GCM. There is no environment variable or configuration file to set — the dashboard and the `PUT /api/merchant/opennode-key` endpoint above are the only ways to configure it. ::: :::info Production keys only The hosted platform connects to OpenNode's production API, so configure a **production** OpenNode key (from [app.opennode.com](https://app.opennode.com)). Development-environment keys will fail validation. ::: ## API Key Security - Never commit your OpenNode key to source control or paste it into client-side code — the only place it belongs is the Lightning Enable dashboard or the `PUT /api/merchant/opennode-key` call. - If you keep a copy, store it in a password manager or secret store. - Lightning Enable never returns your OpenNode key back out of the API once saved. - If you suspect the key leaked, revoke it in the OpenNode dashboard immediately and configure a new one. ## Key Rotation Regularly rotate API keys for security: ### When to Rotate - Every 90 days (recommended) - After employee departure - If key may be compromised - After security incident ### Rotation Process 1. **Generate new key** in the OpenNode dashboard 2. **Update the key in Lightning Enable** — dashboard (Settings → Payment Provider) or: ```bash curl -X PUT https://api.lightningenable.com/api/merchant/opennode-key \ -H "X-API-Key: your-merchant-api-key" \ -H "Content-Type: application/json" \ -d '{ "openNodeApiKey": "your-new-opennode-api-key" }' ``` 3. **Validate the new key**: ```bash curl -X POST https://api.lightningenable.com/api/merchant/validate-opennode \ -H "X-API-Key: your-merchant-api-key" ``` 4. **Verify functionality** with a small test payment 5. **Revoke the old key** in the OpenNode dashboard The update takes effect immediately — subsequent payments use the new key, so there is no downtime window to manage. ## Verify API Key ```bash curl -X POST https://api.lightningenable.com/api/merchant/validate-opennode -H "X-API-Key: your-merchant-api-key" ``` This returns HTTP `200` either way; read `isValid` in the body. Lightning Enable checks the key by listing one charge (`GET /v1/charges?limit=1`), which needs the same invoice permission it uses to create charges. Do not test with `/v1/account/balance`: that needs a broader permission, so an invoice-only key fails it while working fine for payments. Saving an OpenNode key also sets OpenNode as your provider if you had not chosen one. To switch later, call `PUT /api/merchant/payment-provider` with `{"provider": "opennode"}` (or `strike`, `nwc`). ## API Key Permissions ### Invoice Key Capabilities - Create Lightning invoices - Check invoice status - List transactions - Get exchange rates ### Admin Key Capabilities Everything in Invoice key, plus: - Create withdrawals - Process refunds - Manage webhooks - Access account settings ### Minimal Permissions Use the least privileged key for your use case: | Feature | Required Key | |---------|--------------| | Accept payments | Invoice | | Check payment status | Invoice | | Process refunds | Admin | | Send payments | Admin | ## Troubleshooting ### Invalid API Key ```json { "error": "Invalid API key" } ``` **Solutions:** 1. Verify key is copied correctly (no extra spaces) 2. Confirm it is a **production** key from [app.opennode.com](https://app.opennode.com) — the hosted platform does not use OpenNode's dev environment 3. Check key hasn't been revoked 4. Generate new key if needed ### Permission Denied ```json { "error": "Insufficient permissions" } ``` **Solutions:** 1. Check key type (Invoice vs Admin) 2. Generate key with appropriate permissions 3. Verify account is fully verified ### Key Not Found ```json { "error": "API key required" } ``` **Solutions:** 1. Check the Authorization header format 2. Verify a key is saved in Lightning Enable (the dashboard, or `GET /api/merchant/me` — check `hasOpenNodeKey`) 3. Re-save the key via the dashboard or `PUT /api/merchant/opennode-key` ## Best Practices Checklist - [ ] Configure the key only via the Lightning Enable dashboard or `PUT /api/merchant/opennode-key` - [ ] Keep any copy in a password manager or secret store, never in source control - [ ] Enable 2FA on OpenNode account - [ ] Use Invoice key for payments (minimal permissions) - [ ] Use Admin key only when refunds needed - [ ] Rotate keys every 90 days - [ ] Monitor for unauthorized usage ## Next Steps - [Webhooks](/opennode-setup/webhooks) - Configure payment notifications - [Testing](/opennode-setup/testing) - Test your integration - [Quick Start](/getting-started/quick-start) - Make your first payment ============================================================================== # Testing Source: https://docs.lightningenable.com/opennode-setup/testing ============================================================================== # Testing Your Integration :::warning Legacy provider This page covers testing against OpenNode, a legacy per-merchant option. Strike is the default payment provider, and new accounts should start there. Unless you have a specific reason to stay on OpenNode, see [Testing Your Strike Integration](/strike-setup/testing). ::: This guide walks you through testing your Lightning Enable integration with OpenNode as your payment provider. ## How Testing Works on the Hosted Platform Lightning Enable is a hosted platform at `api.lightningenable.com`, and it connects to OpenNode's **production** API. That means: - Your OpenNode API key must be a **production** key from [app.opennode.com](https://app.opennode.com) (KYB required). - Test payments are **small real mainnet payments** (e.g., $1). Lightning fees make this cheap, and you keep the funds in your own OpenNode account. - OpenNode's dev/testnet environment ([app.dev.opennode.com](https://app.dev.opennode.com), `dev-api.opennode.com`) is useful for exploring OpenNode's own API directly, but dev keys **cannot** be used with the hosted platform. | Setting | Value | |---------|-------| | OpenNode Dashboard | [app.opennode.com](https://app.opennode.com) | | Lightning Enable API | `https://api.lightningenable.com` | | Bitcoin Network | Mainnet | | KYB Required | Yes (OpenNode requirement) | ## Setup ### Step 1: Configure Your OpenNode Key Save your production OpenNode API key in Lightning Enable — via the dashboard (**Settings → Payment Provider**) or the merchant API: ```bash curl -X PUT https://api.lightningenable.com/api/merchant/opennode-key \ -H "X-API-Key: your-merchant-api-key" \ -H "Content-Type: application/json" \ -d '{ "openNodeApiKey": "your-opennode-api-key" }' ``` ### Step 2: Validate the Key ```bash curl -X POST https://api.lightningenable.com/api/merchant/validate-opennode \ -H "X-API-Key: your-merchant-api-key" ``` The call returns `200` either way; `"isValid": true` confirms Lightning Enable can list charges with your key. ### Step 3: Get a Lightning Wallet You need a Lightning wallet with a small balance to pay test invoices. Any mainnet Lightning wallet works (e.g., Strike, Phoenix, Breez, Wallet of Satoshi, or your own node). ## Test Payment Flow ### Create Test Payment ```bash curl -X POST https://api.lightningenable.com/api/payments \ -H "X-API-Key: your-merchant-api-key" \ -H "Content-Type: application/json" \ -d '{ "orderId": "TEST-001", "amount": 1.00, "currency": "USD", "description": "Test payment" }' ``` Response: ```json { "invoiceId": "1042", "status": "unpaid", "amount": 1.00, "currency": "USD", "lightningInvoice": "lnbc10u1p...", "payUrl": "https://api.lightningenable.com/pay/", "expiresAt": "2026-06-30T12:00:00Z" } ``` (Abridged.) `payUrl` is a hosted checkout link for this invoice; treat it like a payment link. ### Pay Test Invoice 1. Copy the `lightningInvoice` value 2. Open your Lightning wallet 3. Paste or scan the invoice 4. Confirm payment ### Verify Payment Check payment status: ```bash curl https://api.lightningenable.com/api/payments/1042 \ -H "X-API-Key: your-merchant-api-key" ``` Response after payment: ```json { "invoiceId": "1042", "orderId": "TEST-001", "status": "paid", "paidAt": "2026-06-29T12:05:00Z" } ``` (Abridged.) ## Test Webhooks ### Local Webhook Testing Lightning Enable delivers webhooks to *your* server. To receive them on your development machine before your production endpoint exists, expose your local webhook handler with ngrok: ```bash # Start your webhook handler locally (whatever stack it runs on), # then expose it: ngrok http 3000 ``` Set the ngrok URL and your webhook secret with the merchant API: ```bash curl -X PUT https://api.lightningenable.com/api/merchant/webhook-url \ -H "X-API-Key: your-merchant-api-key" \ -H "Content-Type: application/json" \ -d '{ "webhookUrl": "https://abc123.ngrok.io/webhooks/lightning", "webhookSecret": "your-webhook-secret" }' ``` ### Verify Webhook Received After a test payment, your endpoint receives a flat JSON object (no envelope) with an `X-LightningEnable-Signature` header: ```json { "invoiceId": "1042", "orderId": "TEST-001", "status": "paid", "amount": 1.00, "currency": "USD", "openNodeChargeId": "abc123-...", "paidAt": "2026-06-29T12:05:00Z", "metadata": null } ``` ## Test Refunds Refunds are enabled per account; email [support@lightningenable.com](mailto:support@lightningenable.com) to turn them on. Then generate a Lightning invoice in your wallet for the refund and send it as `refundAddress`: ```bash curl -X POST https://api.lightningenable.com/api/refunds -H "X-API-Key: your-merchant-api-key" -H "Content-Type: application/json" -d '{ "invoiceId": "1042", "refundType": "ln", "refundAddress": "lnbc..." }' ``` Omit `amountSatoshis` for a full refund. Check status with `GET /api/refunds/{refundId}`. Refunds do not send a merchant webhook; poll instead. ## Test L402 (Optional) ### Create L402 Proxy ```bash curl -X POST https://api.lightningenable.com/api/proxy \ -H "X-API-Key: your-merchant-api-key" \ -H "Content-Type: application/json" \ -d '{ "name": "Test API", "targetBaseUrl": "https://httpbin.org", "defaultPriceSats": 10 }' ``` ### Test L402 Flow ```bash # Get a 402 challenge (use the proxyId the create call returned) curl https://api.lightningenable.com/l402/proxy/{proxyId}/get # Pay the invoice from response # ... # Access with L402 credential curl https://api.lightningenable.com/l402/proxy/{proxyId}/get \ -H "Authorization: L402 :" ``` ## Testing Checklist ### Basic Integration - [ ] Create payment successfully - [ ] Receive Lightning invoice - [ ] Pay invoice with a Lightning wallet - [ ] Status updates to "paid" - [ ] Webhook received ### Webhook Integration - [ ] Webhook endpoint accessible - [ ] Signature verification works - [ ] Events processed correctly - [ ] Duplicate handling works ### Refunds (if enabled) - [ ] Create refund successfully - [ ] Refund received in wallet ## Common Test Scenarios ### Test Expired Invoice ```javascript // Create invoice with short expiry. Without expiresInSeconds, OpenNode's // default lifetime is 24 hours; its shortest is 10 minutes (600 seconds). const payment = await createPayment({ orderId: 'TEST-EXPIRE', amount: 1.00, currency: 'USD', expiresInSeconds: 600 }); // Wait until payment.expiresAt has passed, then check the status const status = await getPayment(payment.invoiceId); // status.status === 'expired' ``` ## Debugging ### Check OpenNode Dashboard 1. Log in to [app.opennode.com](https://app.opennode.com) 2. Go to **Transactions** 3. Find your test payments 4. View status and details ### Verify Webhook Delivery Check **your own server logs** to confirm the webhook POST arrived with a valid `X-LightningEnable-Signature` header. If a webhook didn't arrive (or you missed it), don't wait — reconcile against the authoritative status: ```bash # Authoritative payment status curl https://api.lightningenable.com/api/payments/{invoiceId} \ -H "X-API-Key: your-merchant-api-key" # Or force a re-check against your payment provider curl -X POST https://api.lightningenable.com/api/payments/{invoiceId}/sync \ -H "X-API-Key: your-merchant-api-key" ``` ## Going Live After successful testing: 1. **Update your webhook URL** from the ngrok tunnel to your production endpoint (`PUT /api/merchant/webhook-url`) 2. **Verify signature validation** is enabled on your production webhook handler 3. **Run one more small real payment** against the production endpoint 4. **Monitor initial transactions** in the OpenNode dashboard and your own logs ## Next Steps - [Quick Start](/getting-started/quick-start) - Integration guide - [Payments API](/api-reference/payments) - API reference - [Webhooks](/api-reference/webhooks) - Webhook documentation ============================================================================== # Webhook Configuration Source: https://docs.lightningenable.com/opennode-setup/webhooks ============================================================================== # Webhook Configuration :::warning Legacy provider This page covers webhooks for OpenNode, a legacy per-merchant option. Strike is the default payment provider, and new accounts should start there. Unless you have a specific reason to stay on OpenNode, see [Strike Webhooks](/strike-setup/webhooks). ::: Lightning Enable forwards OpenNode payment events to your callback URL, signed with your webhook secret. ## How It Works 1. **OpenNode → Lightning Enable.** Each charge Lightning Enable creates carries its own callback URL, so you configure nothing in the OpenNode dashboard. 2. **Lightning Enable → your server.** Lightning Enable forwards a signed, flat JSON payload to the URL you set below. ## Configure Webhooks ### Step 1: Set Your Webhook URL and Secret Generate a secret (for example, `openssl rand -hex 32`), keep a copy, and set both with the merchant API. Lightning Enable never returns the secret, and your account's initial secret is not readable, so set your own. ```bash curl -X PUT https://api.lightningenable.com/api/merchant/webhook-url -H "X-API-Key: your-merchant-api-key" -H "Content-Type: application/json" -d '{ "webhookUrl": "https://your-site.com/webhooks/lightning", "webhookSecret": "your-webhook-secret" }' ``` ### Step 2: Verify the Signature Every delivery carries `X-LightningEnable-Signature: t={timestamp},v1={hex_hmac}`. The HMAC-SHA256 is computed with your secret over `{timestamp}.{raw_body}`. Reject timestamps older than 5 minutes and compare in constant time. Use the reference verifiers in [Verifying Webhooks](/api-reference/webhooks#verifying-webhooks) rather than writing your own. ## Webhook Payload Lightning Enable forwards a **flat JSON object** per payment event — there is no envelope and no `event` field. For OpenNode-configured merchants the payload is: ```json { "invoiceId": "1042", "orderId": "ORDER-12345", "status": "paid", "amount": 49.99, "currency": "USD", "openNodeChargeId": "abc123-def456-...", "paidAt": "2026-07-03T12:05:00Z", "metadata": "{\"customerId\":\"cust_42\"}" } ``` - `status` is the payment status; route on it. `paid` is the fulfillment trigger. You may also see `processing` (which includes OpenNode's `underpaid`), `expired`, and `refunded`. - `invoiceId` is the Lightning Enable invoice ID (numeric string) from `POST /api/payments`. - `metadata` is the metadata **JSON string** you supplied at payment creation (or `null`). - Refunds do not generate merchant webhooks — poll `GET /api/refunds/{refundId}` instead. (On Strike or NWC the payload carries `providerChargeId` and `provider` in place of `openNodeChargeId`; see the [Webhooks API reference](/api-reference/webhooks).) ## Webhook Requirements ### Endpoint Requirements - **HTTPS** in production (HTTP allowed for localhost) - Return **200-299** status code within **10 seconds** (the delivery request times out after 10s) - Accept **POST** requests with **JSON** body ### Headers Sent | Header | Description | |--------|-------------| | `Content-Type` | `application/json` | | `X-LightningEnable-Signature` | Timestamped HMAC-SHA256 signature (`t={timestamp},v1={hmac}`) | ## Delivery & Reliability Treat webhooks as a fast-path notification, not a guaranteed delivery channel: - Each delivery attempt has a **10-second timeout**; the first attempt fires as soon as Lightning Enable processes OpenNode's webhook. - If your endpoint is down or errors, delivery is retried with exponential backoff (30s, 60s, 120s, 240s, 480s). Every attempt sends identical payload bytes with a freshly timestamped signature, so dedupe on `invoiceId` + `status`, never on the signature header. - After retries exhaust, the event is marked permanently failed — recover by polling. **Recovery pattern:** reconcile pending orders via `GET /api/payments/{invoiceId}` (authoritative), or force a provider re-check with `POST /api/payments/{invoiceId}/sync`. ## Testing Webhooks ### Local Development Use ngrok to expose your local server: ```bash # Terminal 1: Start your server npm start # http://localhost:3000 # Terminal 2: Start ngrok ngrok http 3000 # Copy the ngrok URL (e.g., https://abc123.ngrok.io) # Use as your webhook URL ``` ### Send Test Webhook To send a properly signed test webhook: ```bash # Set your variables WEBHOOK_SECRET="your-webhook-secret" TIMESTAMP=$(date +%s) PAYLOAD='{"invoiceId":"1042","orderId":"TEST-001","status":"paid","amount":10.00,"currency":"USD","openNodeChargeId":"test-charge","paidAt":"2026-07-03T12:05:00Z","metadata":null}' # Compute the signature SIG=$(echo -n "${TIMESTAMP}.${PAYLOAD}" | openssl dgst -sha256 -hmac "${WEBHOOK_SECRET}" | cut -d' ' -f2) # Send the webhook curl -X POST https://your-ngrok-url.ngrok.io/webhooks/lightning \ -H "Content-Type: application/json" \ -H "X-LightningEnable-Signature: t=${TIMESTAMP},v1=${SIG}" \ -d "${PAYLOAD}" ``` ## Troubleshooting ### Webhooks Not Received 1. **Check URL** - Verify webhook URL is correct 2. **Check HTTPS** - Production requires HTTPS 3. **Check firewall** - Allow incoming connections 4. **Check your server logs** - and reconcile missed events via `POST /api/payments/{invoiceId}/sync` ### Signature Mismatch 1. **Raw body** - Use the raw request body, not re-serialized JSON 2. **Correct secret** - Verify webhook secret matches what you configured 3. **Correct header** - The header is `X-LightningEnable-Signature`, not `X-Webhook-Signature` 4. **Signed payload format** - HMAC is computed over `{timestamp}.{payload}`, not just the payload 6. **Encoding** - Use UTF-8 encoding ### Timeouts and Duplicates Return `2xx` within 10 seconds and do slow work afterward. Deliveries can repeat, so make your handler a no-op for an `invoiceId` + `status` you have already processed. ## Next Steps - [Testing](/opennode-setup/testing) - Test your integration - [Webhooks API](/api-reference/webhooks) - API reference - [First Payment](/getting-started/first-payment) - Complete a test payment ============================================================================== # Premium Guides Source: https://docs.lightningenable.com/premium-guides ============================================================================== # Premium Guides Deep-dive technical guides for serious builders. Pay once with Lightning, access forever. Prices are in USD; you pay the sats equivalent at the live BTC/USD rate.
## Build L402 from Scratch **$3** Implement the L402 protocol yourself, no platform required: - Protocol deep dive (macaroons, payment hashes, preimages) - Full implementations in Node.js, Python, and Go - Lightning node integration (LND) - Production hardening (replay prevention, rate limiting, key management) Buy with Lightning ($3) --- ## Building Revenue-Positive Agents **$3** The complete economics guide for AI agents that earn more than they spend: - Cost modeling for AI services - Pricing strategies - Budget policy implementations - Real P&L examples - Path to self-sustaining agents Buy with Lightning ($3) --- ## Advanced NWC Implementation **$3** Production-grade NWC patterns for agents that pay at scale: - Custom relay infrastructure (strfry, nginx + TLS) - High-throughput payment orchestration (connection pooling, concurrency) - NIP-04 vs NIP-44 v2 encryption compatibility (CoinOS gotcha and others) - Multi-wallet failover with circuit breakers, gated on preimage support - Monitoring, health checks, and structured logging - Security hardening (secrets management, domain allowlisting, spending guards) Buy with Lightning ($3) --- ## Multi-Agent Payment Networks **$3** Agents delegating budgets to sub-agents, hierarchical spending policies, and inter-agent commerce: - Hierarchical budget delegation with revocation - Inter-agent L402 payment routing - Spending policy trees (org → team → agent) - Audit trails and reconciliation - Complete research coordinator example with 5 specialist agents Buy with Lightning ($3) --- ## L402 for Streaming and WebSocket APIs **$3** Monetize real-time data feeds and persistent connections with L402: - Token refresh patterns for ongoing streams - Metered streaming (pay per minute, message, or MB) - WebSocket L402 handshake (two-phase connection pattern) - Server-Sent Events with periodic re-auth - Prepaid credit model - Complete market data feed example (Node.js server + Python client) Buy with Lightning ($3) --- ## Lightning Payment Compliance Playbook **$3** The regulatory and compliance guide for platforms accepting Lightning payments: - MSB/MTL requirements (US), MiCA (EU) post-enforcement reality, global patterns - Custody models and compliance implications - KYB/KYC requirements and enterprise onboarding - Transaction monitoring and reporting thresholds - Tax considerations including Form 1099-DA (live for tax year 2025) - Enterprise checklist (SOC2, PCI-DSS, vendor risk templates) - Template Terms of Service for Lightning payments - Payment processor evaluation framework Buy with Lightning ($3) --- ## Building an L402 Marketplace **$3** Build a multi-vendor API marketplace powered by L402 micropayments: - Marketplace proxy architecture with revenue splitting - Vendor onboarding and endpoint verification - Catalog and discovery (REST + agent-friendly format) - Multi-vendor L402 proxy with per-vendor pricing - Rating, trust, and uptime monitoring - Complete 3-vendor marketplace example - Scaling considerations and abuse prevention Buy with Lightning ($3) --- ## Agent Fleet Economics **$3** Managing 10-100+ AI agents with centralized budget allocation and fleet-wide P&L: - Centralized budget allocation (equal, performance-weighted, demand-based) - Wallet architecture for fleets (pooled wallet with virtual ledger) - Automated wallet funding with tiered sources - Fleet-wide P&L dashboard - Cost anomaly detection with automatic circuit breakers - Agent lifecycle management (provision, scale, decommission) - Complete 20-agent content agency example - Optimization strategies (34% cost reduction patterns) Buy with Lightning ($3)
--- ## Bundle: All Guides **$20** — Save $4 Get all eight premium guides at a discount. Buy Bundle with Lightning ($20) --- ## For AI Agents Agents can buy guides over L402. List them for free, then request one: ```bash # 1. List guides and their endpoints (free) curl https://api.lightningenable.com/api/l402/content # 2. Request a guide (returns 402 with an invoice and macaroon) curl https://api.lightningenable.com/api/l402/content/building-revenue-positive-agents # 3. Pay the invoice, then retry with the L402 credential curl https://api.lightningenable.com/api/l402/content/building-revenue-positive-agents -H "Authorization: L402 {macaroon}:{preimage}" ``` The 402 response states the price in sats at the live BTC/USD rate. --- export const LightningCheckoutStyles = () => ( ); ============================================================================== # AI Agent Integration Source: https://docs.lightningenable.com/products/agentic-commerce/ai-agent-integration ============================================================================== # AI Agent Integration The open-source (MIT) Lightning Enable MCP server lets an AI agent such as Claude pay Lightning invoices and L402-protected APIs from a wallet you connect. Paying needs only a wallet; selling access (`l402_producer`) and some agent-marketplace actions need a Lightning Enable API key. ## How auto-pay works When the agent calls `access_l402_resource` on a URL that answers `402 Payment Required`, the MCP server: 1. Reads the Lightning invoice (and macaroon) from the 402 response. 2. Checks the price against your budget limits, and asks you for a confirmation code if the payment is above your auto-approve threshold. 3. Pays the invoice from your wallet and gets the preimage. 4. Retries the request with `Authorization: L402 :` and returns the response. ``` User → Agent → MCP server → L402 API ↓ Your Lightning wallet (LND, NWC, or Strike) ``` L402 needs a wallet that returns the preimage: LND, Strike, or an NWC wallet such as CoinOS, CLINK, or Alby Hub. OpenNode and Primal can pay plain invoices with `pay_invoice` but can't complete L402. ## Set it up 1. **Install and connect a client.** Follow [MCP Quick Start](/products/agentic-commerce/mcp-quickstart), or the client guides for [Claude Code](/products/agentic-commerce/claude-code-setup), [Cursor](/products/agentic-commerce/cursor-setup), [Hermes](/products/agentic-commerce/hermes-nwc-setup), or [Buzz](/products/agentic-commerce/buzz-agent-setup). 2. **Pick a wallet.** See [Wallet Configuration](/products/agentic-commerce/mcp-wallet-setup) or [LND Node Setup](/products/agentic-commerce/lnd-setup). 3. **Set spending limits** in `~/.lightning-enable/config.json` before the first payment. See [AI Spending Security](/products/agentic-commerce/ai-spending-security). ## Tools you'll use most | Tool | What it does | |---|---| | `access_l402_resource` | Fetch a URL and pay its L402 challenge automatically | | `pay_l402_challenge` | Pay an invoice (and macaroon) you already hold; returns the credential | | `pay_invoice` | Pay any BOLT11 invoice and return the preimage | | `discover_api` | Search the L402 registry, or read one API's manifest and prices | | `get_balance` | Wallet balance and session spend | | `budget` | Read limits (`action="status"`) or lower them for this session (`action="tighten"`) | | `receipts` | List payments (`source="durable"` or `source="session"`) | The [MCP Complete Guide](/products/agentic-commerce/mcp-complete-guide) lists every tool, its parameters, and the tool profiles. ## Example ``` User: Get the 7-day forecast from the Lightning weather API at https://api.example.com/l402/proxy/weather/forecast/7day Agent: [Uses access_l402_resource] The forecast cost 25 sats, paid from your wallet. Here it is: ... ``` ## Building your own integration Outside an MCP client, use the L402 client libraries. They detect a wallet from the same environment variables (LND > NWC > Strike > OpenNode). **Python** ([l402-requests](https://pypi.org/project/l402-requests/)): ```python # pip install l402-requests from l402_requests import AsyncL402Client async with AsyncL402Client() as client: response = await client.get("https://api.example.com/paid-resource") print(response.json()) ``` **.NET** ([L402Requests](https://www.nuget.org/packages/L402Requests)): ```csharp // dotnet add package L402Requests using L402Requests; using var client = new L402HttpClient(new L402Options { MaxSatsPerRequest = 1000 }); var response = await client.GetAsync("https://api.example.com/paid-resource"); Console.WriteLine(await response.Content.ReadAsStringAsync()); ``` Both packages document their budget controls and domain allowlists. ## Next steps - [MCP Complete Guide](/products/agentic-commerce/mcp-complete-guide) - full tool and configuration reference - [AI Spending Security](/products/agentic-commerce/ai-spending-security) - read before letting an agent pay - [How It Works](/products/agentic-commerce/how-it-works) - the L402 protocol - [Proxy Configuration](/products/agentic-commerce/proxy-configuration) - put your own API behind L402 ============================================================================== # AI Spending Security Source: https://docs.lightningenable.com/products/agentic-commerce/ai-spending-security ============================================================================== # AI Spending Security When you connect a wallet to the Lightning Enable MCP server, you let an AI agent spend Bitcoin on your behalf. Every paying tool spends from that wallet: `pay_invoice`, `access_l402_resource`, `pay_l402_challenge`, `wallet_ops` with `action="send_onchain"`, `agent_services` with `action="settle"`, `create_lightning_enable_account`, and `renew_lightning_enable_subscription`. This page covers what can go wrong and how to bound it. The exact config keys are in the [MCP Complete Guide](/products/agentic-commerce/mcp-complete-guide#budget-configuration). ## What can go wrong - **Unintended payments.** The agent misreads your intent, pays the wrong invoice, pays more than expected, or pays twice. - **Prompt injection.** Content the agent reads (a web page, an API response, a marketplace listing) tells it to pay an attacker's invoice. - **Credential leaks.** A wallet API key, NWC connection string, or LND macaroon ends up in logs, screenshots, or version control. - **Budget draining.** Many small payments, each under a limit, add up. ## Safeguards ### 1. Use a dedicated, small wallet Never connect your main wallet or business funds. Give the agent its own wallet holding only what you are willing to lose; the balance is the hard ceiling no prompt can raise. Where the wallet supports it, also set a spend cap on the connection itself (for example, an Alby Hub per-connection budget). L402 needs a wallet that returns the payment preimage: LND, Strike, or an NWC wallet such as CoinOS, CLINK, or Alby Hub. OpenNode and Primal can pay plain invoices but not L402 challenges. See [Wallet Configuration](/products/agentic-commerce/mcp-wallet-setup). ### 2. Set limits in the config file before first use Limits live in `~/.lightning-enable/config.json` (on Windows, `C:\Users\\.lightning-enable\config.json`). The server creates it on first run with conservative defaults (see [Budget Configuration](/products/agentic-commerce/mcp-complete-guide#budget-configuration)); open the file and set your own: ```json { "currency": "USD", "tiers": { "autoApprove": 0.10, "logAndApprove": 1.00, "formConfirm": 10.00, "urlConfirm": 100.00 }, "limits": { "maxPerPayment": 5.00, "maxPerSession": 20.00, "maxPerPaymentSats": 5000 }, "session": { "cooldownSeconds": 2, "requireApprovalForFirstPayment": true } } ``` - Payments up to `autoApprove` go through; up to `logAndApprove` they go through and are logged; above that they need a confirmation code from you. - Anything above `maxPerPayment`, or beyond `maxPerSession` in total, is refused. - USD values are converted at payment time; if no BTC price source answers, the payment is refused. Sats keys (`maxPerPaymentSats`, `maxPerSessionSats`, `autoApproveSats`) need no price feed. When both are set, the stricter wins. - Set `autoApprove` to `0` to confirm every payment. No MCP tool can raise these limits. The agent can only lower its runtime caps with `budget(action="tighten")`. To raise a limit, edit the file and restart the server. The `LIGHTNING_ENABLE_MAX_PER_PAYMENT_SATS`, `LIGHTNING_ENABLE_MAX_PER_SESSION_SATS`, and `LIGHTNING_ENABLE_AUTO_APPROVE_SATS` environment variables override the file, so treat the MCP client config that sets them with the same care. ### 3. Approve larger payments out of band When a payment needs confirmation: 1. The payment tool returns `requiresConfirmation` without the code. 2. The server delivers the code to you: by default it prints it to its console (stderr). 3. You give the code to the agent, which calls the **same** payment tool again with `confirmationNonce` (.NET) or `confirmation_nonce` (Python). The code is never returned in a tool result, so a prompt-injected agent can't approve its own payment. A code is bound to the exact amount, tool, and destination, and expires after two minutes by default. `verify_confirmation_code` only checks what a code authorizes; it never pays. On-chain sends (`wallet_ops` with `action="send_onchain"`) always need a code. #### Confirmation channels Set `confirmation.channel` (or `LIGHTNING_ENABLE_CONFIRMATION_CHANNEL`) to choose where the code goes: | Channel | Where the code goes | Use it for | |---|---|---| | `stderr` (default) | The server console | Interactive use in Claude Code, Claude Desktop, or a terminal | | `refuse` | Nowhere; the payment is refused | Unattended or hosted runs | | `webhook` | A signed POST to `confirmation.webhookUrl` | Notifying you in a chat app or pager | | `file` | `confirmation.filePath` | Headless runs you check later | In a hosted deployment, set `LIGHTNING_ENABLE_HOSTED=1`: when stdin is not a terminal, the default becomes `refuse`. Details are in the [MCP Complete Guide](/products/agentic-commerce/mcp-complete-guide#out-of-band-confirmation). ### Threat model The confirmation code and the config file protect you only if **the agent can't read the MCP server's stderr or logs, and can't edit files on the server's host.** That holds when Claude Desktop or an IDE starts the server as a subprocess. It does not hold for an agent with shell or filesystem access to the same machine: it could read its own code or edit `config.json`. For those agents, run the MCP server where the agent can't reach it (a separate user, host, or container), or use the `refuse` channel. ### 4. Protect wallet credentials - Create a dedicated key or connection for the agent; don't reuse one. - Give it the narrowest permissions the wallet offers (for NWC, pay and balance only, plus a budget). - Keep keys out of version control, screenshots, and chat. Prefer the global client config or the config file over a project file you might commit. - Rotate the key or connection if it may have leaked, and when you stop using it. ### 5. Review what was spent After each session, ask the agent for `receipts` with `source="durable"` (the append-only log at `~/.lightning-enable/receipts.jsonl`), and compare it with your wallet's own history. Turn on your wallet's payment notifications and low-balance alerts if it has them. ## When not to let an agent pay Don't connect a wallet if the agent will run unattended for long periods with a confirmation channel nobody watches, if you haven't set limits, if the wallet holds business funds, or if you are using an MCP configuration you didn't write. Good fits are supervised development, testing, and small, bounded research tasks. ## Where the money is Lightning Enable does not hold funds. The MCP server is software that runs on your machine and calls your wallet's API: Strike or OpenNode (which facilitate custody and settlement for your account), your own LND node, or an NWC wallet. Lightning Enable never sees your wallet credentials or your Bitcoin, and any account verification is between you and your wallet provider. ## If you suspect unauthorized payments 1. Revoke or rotate the wallet key, NWC connection, or macaroon immediately. 2. Check payment history in your wallet's own dashboard. 3. Compare it with the durable receipts log. 4. Update the MCP configuration with the new credential. 5. Lower your limits, and work out how the payment was triggered before reconnecting. ## Related - [MCP Complete Guide](/products/agentic-commerce/mcp-complete-guide) - every config key and tool - [AI Spending Guidelines](/configuration/ai-spending-guidelines) - suggested limits by use case - [Legal Considerations](/configuration/legal-considerations) - liability and terms ============================================================================== # Monetize Your API in 10 Minutes Source: https://docs.lightningenable.com/products/agentic-commerce/api-monetization ============================================================================== # Monetize Your API in 10 Minutes Lightning Enable turns any HTTP API into a paid API. Every request either includes a valid payment token or gets back a 402 with a Lightning invoice. You pick how it sits in the request path. ## Two integration modes — pick one Lightning Enable supports two ways to put L402 in front of your API: | | **Native mode** (recommended for production APIs) | **Proxy mode** (no code changes) | |---|---|---| | Where traffic lives | On your domain, your servers | Routed through `api.lightningenable.com` | | Code changes | One `install` + one line of middleware | None | | Existing auth, rate limiting, observability | Preserved — chain your own middleware around it | LE consumes `Authorization`, harder to pass through | | Custom logic per route | Full control | Limited to what the proxy supports | | Best for | Commercial APIs, anything with sensitive infrastructure | Public APIs, experiments, demos | **If your API is something you already run in production — and especially if it has its own auth — Native mode is the right answer.** That's the path this Quick Start walks. If you want zero code changes and you're fine with traffic going through Lightning Enable, jump to [Alternative: hosted proxy mode](#alternative-hosted-proxy-mode-no-code-changes) below. ## Prerequisites - A Lightning Enable account. The **Free Producer Sandbox** (no card; 3 endpoints, 200 challenges a month, 1,000 sats max per challenge) is enough to follow this guide. **Agentic Commerce** ($49/mo, 30-day free trial) removes the caps; **Agentic Commerce — Business** is [contact us](mailto:support@lightningenable.com). - A payment provider — **Strike** (recommended), **OpenNode**, or your own Lightning wallet over **Nostr Wallet Connect**. - An existing HTTP API on a supported stack (Node + Express or .NET + ASP.NET Core today; FastAPI and Go on the roadmap — see the [Producer API Reference](./producer-api-reference) for raw HTTP integration in any language). ## Check your setup state at any time If you want to know exactly where you are in the 10-minute setup — what's done, what's still required, what example payload comes next — hit the quick-start endpoint: ```bash curl https://api.lightningenable.com/api/merchant/quickstart \ -H "X-API-Key: YOUR_LE_API_KEY" ``` You get back a state machine with `completedSteps`, `requiredStepsCompleted`, `isReadyForProduction`, and a per-step breakdown. Useful both as a "did I miss something" sanity check and as a programmatic readiness probe. ## Step 1: Configure your payment provider (minutes 1-3) Strike is the default and recommended provider. You can also use OpenNode, or receive straight into your own wallet over Nostr Wallet Connect. ### Option A: Strike (recommended) 1. Create an account at [strike.me](https://strike.me) and get your API key from [dashboard.strike.me](https://dashboard.strike.me) 2. Add it to Lightning Enable: ```bash curl -X PUT https://api.lightningenable.com/api/merchant/strike-key \ -H "X-API-Key: YOUR_LE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"strikeApiKey": "your-strike-api-key"}' ``` Response: ```json { "success": true, "message": "Strike API key updated successfully. Webhook will re-register on the next L402 challenge." } ``` ### Option B: OpenNode (alternative) Generate an API key at [opennode.com](https://www.opennode.com) and add it: ```bash curl -X PUT https://api.lightningenable.com/api/merchant/opennode-key \ -H "X-API-Key: YOUR_LE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"openNodeApiKey": "your-opennode-api-key"}' ``` OpenNode KYB verification takes 2-4 business days. The OpenNode environment (dev or production) is set platform-wide, not per merchant, and hosted Lightning Enable runs against production, so a dev (testnet) key won't create invoices here. To test with a dev key, contact support. ### Option C: your own wallet (Nostr Wallet Connect) Paste the `nostr+walletconnect://` string from your wallet app: ```bash curl -X PUT https://api.lightningenable.com/api/merchant/nwc-connection \ -H "X-API-Key: YOUR_LE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"nwcConnectionString": "nostr+walletconnect://..."}' ``` NWC is receive-only. See [Nostr Wallet Connect setup](/nwc-setup/account-setup) for supported wallets. ## Step 2: Install the middleware (minutes 4-5) Pick your stack: ### Node.js + Express ```bash npm install l402-express l402-server ``` `l402-server` is the underlying SDK that `l402-express` calls; both are MIT-licensed and the middleware re-exports the SDK's types. ### .NET + ASP.NET Core ```bash dotnet add package L402Server.AspNetCore ``` The underlying `L402Server` SDK is pulled in transitively. Both are MIT-licensed. ### Another stack? The producer API is HTTP — you can integrate from any language. See the [Producer API Reference](./producer-api-reference) for the two endpoints you'll call (`POST /api/l402/challenges` to mint, `POST /api/l402/challenges/verify` to validate). ## Step 3: Add the middleware + set prices (minutes 6-7) One line in your app. Anything mounted under it costs the configured number of sats per request. ### Express ```js import express from "express"; import { l402 } from "l402-express"; const app = express(); // Anything below this costs 100 sats per request app.use("/api/premium", l402({ apiKey: process.env.LIGHTNING_ENABLE_API_KEY, priceSats: 100, })); app.get("/api/premium/weather", (_req, res) => { res.json({ temp: 72 }); }); app.listen(3000); ``` Endpoints **not** mounted under `l402(...)` pass through untouched — mix paid and free routes freely. For variable per-request pricing, pass a function instead of a number: `priceSats: (req) => req.query.model === "premium" ? 500 : 100`. ### ASP.NET Core ```csharp using L402Server.AspNetCore; var builder = WebApplication.CreateBuilder(args); builder.Services.AddL402AspNetCore(opts => { opts.ApiKey = builder.Configuration["LightningEnable:ApiKey"]!; }); var app = builder.Build(); app.UseRouting(); app.UseL402(); app.MapControllers(); app.Run(); ``` Then mark any action with `[L402(PriceSats = N)]`: ```csharp [ApiController] [Route("api/premium")] public class PremiumController : ControllerBase { [HttpGet("weather")] [L402(PriceSats = 100)] public IActionResult Weather() => Ok(new { temp = 72 }); } ``` That's the whole integration. For both stacks, the middleware: 1. Reads `Authorization: L402 :` from each request 2. If absent → mints a fresh challenge and returns `402 Payment Required` with the invoice 3. If present → verifies it via Lightning Enable. Valid → call your handler; invalid → respond `401` ## Step 4: Test it (minutes 8-9) ### Try an unpaid request ```bash curl -i https://your-api.example/api/premium/weather ``` You'll get: ```http HTTP/1.1 402 Payment Required Content-Type: application/json WWW-Authenticate: L402 macaroon="AgEL...", invoice="lnbc..." { "error": "Payment Required", "l402": { "macaroon": "AgEL...", "invoice": "lnbc1u1p3...", "amount_sats": 100, "payment_hash": "abc123...", "expires_at": "", "resource": "/api/premium/weather" } } ``` ### Pay and access ```bash # After paying the invoice and extracting the preimage: curl https://your-api.example/api/premium/weather \ -H "Authorization: L402 AgEL...:deadbeef..." # 200 OK — your API's response ``` ## Step 5: Ship it (minute 10) Deploy your app. Your existing API is now a paid API. Give your callers (or their agents) the URL — they hit it, pay 402 invoices, get access. ## How L402 interacts with your existing auth The most common question: **does L402 replace my auth, or sit next to it?** Two cases — both work: - **L402 is your only auth.** The macaroon's caveats (path, amount, merchant ID, expiry) plus preimage verification *are* the authorization. Anyone who pays for that resource gets in. This is the right model for the ~90% case — paid APIs where "they paid" is the only fact you need to enforce. L402 is anonymous-payment by design, so this is what you get out of the box. - **L402 + your existing customer auth, side by side.** They coexist because they live in **different request fields**. L402 owns `Authorization: L402 :`. Your existing auth uses a cookie, `X-API-Key`, a JWT in a separate header, an IP allowlist, mTLS — whatever you're already doing. Chain your own auth middleware around `l402(...)` (Express) or before/after `app.UseL402()` (ASP.NET Core); they don't fight. After successful verification, the middleware exposes the verified credential to your handler: - **Express:** `res.locals.l402` → `{ resource, amountSats, paymentHash }` - **ASP.NET Core:** `HttpContext` items — same fields Useful for usage logging, per-endpoint analytics, fraud detection. The payment hash gives you a stable per-payment identifier without revealing buyer identity. ## Optional: list your API in the registry If you want AI agents to *discover* your paid API (not just successfully call it once they know the URL), publish a manifest in the L402 registry. This step is independent of the middleware and uses the existing proxy management endpoints — see [Setting Up Your Proxy](./proxy-setup-walkthrough) for the manifest and registry portion (skip the upstream URL bits; in Native mode your manifest points at your own host). ## Alternative: hosted proxy mode (no code changes) If you'd rather not touch your application code, Lightning Enable can sit in front of your API as a hosted proxy. You give us your API URL, we give you a `https://api.lightningenable.com/l402/proxy/{your-slug}/` URL that handles the 402 challenge dance and forwards paid requests to your origin. - **Pros:** zero code changes, no deploy required, fastest path to a working paywall - **Cons:** traffic flows through Lightning Enable, your existing `Authorization`-header auth is hard to preserve, customer domain changes Full walkthrough: [Setting Up Your Proxy](./proxy-setup-walkthrough). ## What Lightning Enable handles (both modes) | Concern | Handled | |---------|---------| | Invoice creation | Via your configured payment provider (Strike, OpenNode, or NWC) | | Payment verification | Preimage validated against payment hash cryptographically | | Token management | Macaroon mint with caveats (path, amount, merchant ID, expiry) | | Tenant isolation | A token for Merchant A can never verify against Merchant B | | Token reuse within validity window | Same payment hash re-verifiable for same resource until expiry (feature, not bug; see [producer API reference](./producer-api-reference#token-reuse-within-the-validity-window)) | | SSRF protection (proxy mode) | Private IPs blocked, target URLs validated | | Analytics | Tracks requests, revenue, endpoint usage | ## Next steps - [Native Integration — Express](./native-integration-express) — full reference for the Node middleware - [Native Integration — ASP.NET Core](./native-integration-aspnet) — full reference for the .NET middleware - [Producer API Reference](./producer-api-reference) — raw HTTP API if you're on a stack we don't have a middleware for yet - [Native Integration overview](./native-integration) — deeper architectural rationale - [Setting Up Your Proxy](./proxy-setup-walkthrough) — full proxy-mode walkthrough if you chose that path ============================================================================== # Buzz Agent Setup Source: https://docs.lightningenable.com/products/agentic-commerce/buzz-agent-setup ============================================================================== # Give Your Buzz Agents a Lightning Wallet Your agents live in a [Buzz](https://github.com/block/buzz) community. This guide gives one of them a Lightning wallet with the Lightning Enable MCP, so it can **discover** services on the agent marketplace, **pay for and consume** them over Lightning, and **publish** its own — all without a credit card or a human in the loop. If you're running the [NostrWolfe bridge](https://github.com/refined-element/nostrwolfe-bridge), your agents can already *see* the marketplace as cards in a Services channel. This guide adds the ability to *act* on it. :::info Open source The MCP server is free and open source: [github.com/refined-element/lightning-enable-mcp](https://github.com/refined-element/lightning-enable-mcp). Available on NuGet, PyPI, and Docker. ::: ## Overview ``` ┌───────────────────────────────────────────────────────────────┐ │ Buzz agent (ACP) │ │ │ discover · pay · consume · publish │ │ ▼ │ │ Lightning Enable MCP (stdio subprocess) │ │ │ │ │ │ │ ▼ ▼ ▼ │ │ NostrWolfe L402 endpoints your Lightning wallet │ │ marketplace (pay-per-call) (LND / NWC / Strike / …) │ └───────────────────────────────────────────────────────────────┘ ``` The agent gets its abilities from the MCP's tools directly — no plugin or skill required. You wire the MCP in once, set a budget, and the agent does the rest. ## Step 1: Connect a Lightning wallet The MCP moves real money, so it needs a wallet. Any one of these works — pick the one you already have: - **NWC** (Nostr Wallet Connect) — a connection string from Alby Hub, or any NWC wallet. Free and L402-ready. - **Strike** — an API key. L402-ready (returns the preimage L402 needs). - **LND** — your own node (best L402 guarantees). - **OpenNode** — an API key. Works for direct payments, but **not** for the L402 pay-per-call flow this guide is built on — OpenNode doesn't return preimages, so `access_l402_resource` can't complete. Pick one of the three above for agent-marketplace buying. See [MCP Wallet Setup](./mcp-wallet-setup.md) for the full comparison and per-wallet steps. You'll use the resulting credential (`NWC_CONNECTION_STRING`, `STRIKE_API_KEY`, or the `LND_REST_HOST` + `LND_MACAROON_HEX` pair) in Step 3. :::tip Wallet priority When several wallets are configured the MCP picks in order **LND > NWC > Strike > OpenNode** (optimized for L402 preimage support). Override with `WALLET_PRIORITY`. ::: ## Step 2: Install the MCP server ```bash dotnet tool install -g LightningEnable.Mcp ``` This puts a `lightning-enable-mcp` command on your PATH. (Python alternative: `uvx lightning-enable-mcp`. The base Python package covers the NWC wallet used below.) ## Step 3: Wire it into your Buzz agent A Buzz agent takes its MCP servers per session as an `mcpServers` array — `buzz-agent` spawns each as a stdio subprocess and namespaces its tools as `server__tool` (so you'll see `lightning-enable__agent_services` in traces). **Self-hosted `buzz-agent`** — add this to the session config: ```json { "mcpServers": [ { "name": "lightning-enable", "command": "lightning-enable-mcp", "args": [], "env": [ { "name": "NWC_CONNECTION_STRING", "value": "nostr+walletconnect://..." } ] } ] } ``` Swap the `env` entry for `STRIKE_API_KEY`, or the `LND_REST_HOST` + `LND_MACAROON_HEX` pair, if that's your wallet. :::note Managed agents (Buzz desktop app) Add the MCP server in the agent's settings UI — name `lightning-enable`, command `lightning-enable-mcp`, and your wallet env var. The app writes it into the managed-agent config for you. ::: :::warning Keep the credential out of your repo The wallet key can spend money. Set it via the environment, scope it to this agent, and never commit it. `buzz-agent` only passes through the env you explicitly list — nothing else leaks into the MCP subprocess. ::: ## Step 4: Set a spending limit — before anything can spend Your agent now holds a spending capability. Cap it first. Create `~/.lightning-enable/config.json`: ```json { "currency": "USD", "tiers": { "autoApprove": 0.10, "logAndApprove": 0.50, "formConfirm": 2.00, "urlConfirm": 5.00 }, "limits": { "maxPerPayment": 10.00, "maxPerSession": 25.00 }, "session": { "requireApprovalForFirstPayment": false } } ``` These limits are in USD, converted to sats at payment time (sats keys such as `maxPerPaymentSats` also work). `limits.maxPerPayment` caps any single payment and `limits.maxPerSession` caps the total across a run; a payment over either is refused. The `tiers` decide where confirmation starts: anything above `logAndApprove` needs a code printed to the server console, which the agent can't read itself. No MCP tool can raise these limits; an agent can only lower its runtime caps with `budget(action="tighten")`. :::caution Unattended agents A Buzz agent often runs with nobody watching its console. Set `confirmation.channel` to `webhook` (codes go to a URL you choose) or `refuse` (over-threshold payments are refused), and size `logAndApprove` to the payments the agent should make on its own. Keep `session.requireApprovalForFirstPayment` at `false` as in the sample: with it on, the first payment of every session needs a code, and on `refuse` no code is ever delivered, so nothing could pay. See [Confirmation channels](./ai-spending-security.md#confirmation-channels). ::: :::warning Budget Configuration Required Treat the budget as the blast radius: if the agent misbehaves or is steered by a malicious listing, this is the most it can spend. Set it before first use. See [AI Spending Security](./ai-spending-security.md). ::: ## Step 5: Prove it works — discover, pay, consume Ask your agent to buy something cheap and real: > Discover an image-upscaling service, then upscale this image. The agent calls `agent_services` with `action="discover"` to find providers, picks one, and calls `access_l402_resource` on its endpoint, which gets the `HTTP 402`, pays the Lightning invoice from your wallet (within budget), and returns the result. Ask for `receipts` to see the payment, or `budget` with `action="status"` to see what's left. Or hand it a card straight from your bridge's Services channel: find one with `@bridge find`, give the agent the `Endpoint:` URL, and let it pay and fetch. ## What your agents can do now One MCP install unlocks the full loop: - **Discover** — `agent_services` with `action="discover"` or `action="reputation"` (formerly `discover_agent_services` / `get_agent_reputation`), plus `discover_api`. Find providers on the marketplace and check their track record before spending. *(wallet only)* - **Buy & consume** — `access_l402_resource` (auto-pay and fetch), `pay_l402_challenge` (manual), `agent_services` with `action="settle"` (pay an agreed L402 endpoint, formerly `settle_agent_service`). *(wallet only)* — kicking off a formal agreement with `agent_services` `action="request"` (formerly `request_agent_service`) needs a producer key (see below). - **Publish** — `agent_services` with `action="publish"` lists your agent's own service (a `kind:38400` on the marketplace); `l402_producer` with `action="create"` gates it behind Lightning; `agent_services` with `action="attest"` records reviews. Once published, it's discoverable by every other agent — including through the NostrWolfe bridge's Services channel. *(producer key — see below)* - **Wallet & budget** — `get_balance`, `pay_invoice`, `create_invoice`, `budget`, and more. *(wallet only)* See the [MCP Complete Guide](./mcp-complete-guide.md) for every tool, including the table of deprecated per-operation names. :::note What needs a producer key Discovering, paying, and settling are free — wallet only. Two tools are gated by a `LIGHTNING_ENABLE_API_KEY`: **`l402_producer`** (every action — create, verify, and the receive/proxy/publish actions) and **`agent_services`** for four of its seven actions — `request`, `publish`, `unpublish`, and `attest` (`discover`, `settle`, and `reputation` are free). The fastest way to get a key: have the agent call `create_lightning_enable_account` with your email — it activates an account with a ~100-sat Lightning payment from your wallet and saves the key to `~/.lightning-enable/config.json`, unlocking those tools. You can also paste a key from your [Lightning Enable dashboard](https://api.lightningenable.com/dashboard) into that config file or set it as the `LIGHTNING_ENABLE_API_KEY` env var. ::: :::tip Free forever — no card required That 100-sat activation is **not** a subscription. It starts a 30-day Agentic Commerce trial with no card on file, and if you never upgrade, the account moves to the **Free Producer Sandbox**: your key keeps working, capped at 3 endpoints, 200 challenges a month, and 1,000 sats per challenge. Paying for services (the wallet side) doesn't depend on your plan. ::: ## How this fits with the NostrWolfe bridge Two independent pieces, one workflow. The **bridge** makes the marketplace *visible* inside your community — discovery, in the room where your agents already work, with no wallet. The **MCP** gives an individual agent the ability to *act* — pay for what it found, or publish its own service. Run the bridge and your agents can see the market; add the MCP and they can transact in it. Neither depends on the other, and neither hands your keys to a vendor. ## Next steps - [MCP Wallet Setup](./mcp-wallet-setup.md) — choose and connect a wallet - [MCP Complete Guide](./mcp-complete-guide.md) — every tool and capability - [AI Spending Security](./ai-spending-security.md) — budgets and safety - [NostrWolfe bridge](https://github.com/refined-element/nostrwolfe-bridge) — mirror the marketplace into your community ============================================================================== # Claude Code Setup Source: https://docs.lightningenable.com/products/agentic-commerce/claude-code-setup ============================================================================== # Give Claude Code a Lightning Wallet Get Claude Code paying Lightning invoices and accessing L402 APIs in under 3 minutes. For the full MCP setup guide (all clients, all wallet options), see **[MCP Quickstart](./mcp-quickstart.md)**. --- ## Step 1: Get a Strike API Key [Strike](https://strike.me) is the fastest way to get started — no infrastructure required. 1. Create an account in the Strike app or at [strike.me](https://strike.me). 2. Sign in at [dashboard.strike.me](https://dashboard.strike.me), create an API key, and copy it. :::tip Why Strike? Strike requires zero infrastructure, supports L402 (returns preimage), and handles custody. Other options: [NWC wallets](/products/agentic-commerce/mcp-wallet-setup) (CoinOS, Alby Hub, CLINK) or [LND](/products/agentic-commerce/lnd-setup) (self-hosted). ::: --- ## Step 2: Install the MCP Server ```bash # .NET (recommended) dotnet tool install -g LightningEnable.Mcp # Or Python pip install lightning-enable-mcp ``` The base Python install supports NWC wallets on every platform; the old `[nwc]` extra adds nothing. --- ## Step 3: Configure Claude Code Register the server with the `claude mcp add` command: **.NET:** ```bash claude mcp add --transport stdio lightning-enable \ --env STRIKE_API_KEY=your-strike-api-key \ -- lightning-enable-mcp ``` **Python:** ```bash claude mcp add --transport stdio lightning-enable \ --env STRIKE_API_KEY=your-strike-api-key \ -- uvx lightning-enable-mcp ``` Alternatively, create a project-scoped `.mcp.json` in your project root (don't commit API keys): ```json { "mcpServers": { "lightning-enable": { "command": "lightning-enable-mcp", "env": { "STRIKE_API_KEY": "your-strike-api-key" } } } } ``` Restart Claude Code after saving, then run `/mcp` to verify the server is connected. :::note Claude Desktop is different Claude Desktop does **not** use `claude mcp add` — it reads `claude_desktop_config.json` (**macOS:** `~/Library/Application Support/Claude/`, **Windows:** `%APPDATA%\Claude\`, **Linux:** `~/.config/claude/`). See [MCP Quick Start](/products/agentic-commerce/mcp-quickstart#claude-desktop) for the Desktop config format. ::: --- ## Step 4: Test It ``` Check my Lightning balance ``` Claude will call `get_balance` and show your Strike balance. Try also: - `Pay this Lightning invoice: lnbc...` - `Fetch data from https://agent-commerce.store/api/weather/forecast?lat=40.71&lon=-74.00` - `Buy me a Lightning Enable t-shirt from store.lightningenable.com` --- ## Spending Limits Before the first real payment, edit the limits in `~/.lightning-enable/config.json` (the server creates it on first run). See [Set Spending Limits](/products/agentic-commerce/mcp-quickstart#set-spending-limits) for a starting configuration. Payments above your auto-approve threshold need a confirmation code that the server prints to its console. Claude Code can run shell commands on the same machine, so it could in principle edit `config.json` or read files the server writes. Keep the wallet small, and read the [threat model](/products/agentic-commerce/ai-spending-security#threat-model) before connecting real funds. --- ## Available Tools Paying, invoicing, L402, budget, and API-discovery tools need only a wallet. Selling access over L402 and some agent-marketplace actions also need `LIGHTNING_ENABLE_API_KEY`. See the [MCP Complete Guide](/products/agentic-commerce/mcp-complete-guide) for the full tool list. --- ## Next Steps - [MCP Quickstart](./mcp-quickstart.md) — Full setup guide for all MCP clients - [Spending Security](/products/agentic-commerce/ai-spending-security) — Advanced budget controls - [Wallet Options](/products/agentic-commerce/mcp-wallet-setup) — NWC, LND, and other wallets - [Agent Commerce Store](https://agent-commerce.store) — L402 APIs to test with ============================================================================== # Cursor Setup Source: https://docs.lightningenable.com/products/agentic-commerce/cursor-setup ============================================================================== # Add Lightning Payments to Cursor Set up the Lightning Enable MCP server in Cursor so your AI agent can pay Lightning invoices and access L402-protected APIs. --- ## Step 1: Get a Strike API Key 1. Create a [Strike](https://strike.me) account. 2. Sign in at [dashboard.strike.me](https://dashboard.strike.me) and create an API key. Prefer another wallet? See [Wallet Configuration](/products/agentic-commerce/mcp-wallet-setup). --- ## Step 2: Install the MCP Server ```bash # .NET dotnet tool install -g LightningEnable.Mcp # Or Python pip install lightning-enable-mcp ``` The base Python install supports NWC wallets on every platform; the old `[nwc]` extra adds nothing. --- ## Step 3: Configure Cursor You can configure via the UI or by editing the config file directly. ### Option A: Cursor Settings UI 1. Open **Cursor Settings** (gear icon) 2. Go to **Tools & MCP** 3. Click **New MCP Server** 4. Paste the JSON config below ### Option B: Config File Create or edit `.cursor/mcp.json` in your project root (project-specific) or `~/.cursor/mcp.json` (global): **.NET:** ```json { "mcpServers": { "lightning-enable": { "command": "lightning-enable-mcp", "env": { "STRIKE_API_KEY": "your-strike-api-key" } } } } ``` **Python:** ```json { "mcpServers": { "lightning-enable": { "command": "uvx", "args": ["lightning-enable-mcp"], "env": { "STRIKE_API_KEY": "your-strike-api-key" } } } } ``` :::warning Don't commit API keys. Use the global config (`~/.cursor/mcp.json`) for credentials, or use environment variables set in your shell profile. ::: Fully restart Cursor after saving (not just close the window). --- ## Step 4: Verify Open **Cursor Settings > Tools & MCP**. You should see `lightning-enable` listed with a green status indicator and its tools. Setting `LIGHTNING_ENABLE_API_KEY` also enables `l402_producer` and the `agent_services` actions `request`, `publish`, `unpublish`, and `attest`. Before the first real payment, set spending limits in `~/.lightning-enable/config.json`; see [Set Spending Limits](/products/agentic-commerce/mcp-quickstart#set-spending-limits). Or ask in Cursor's Agent mode: ``` What MCP tools do you have available? ``` ### Test a Payment ``` Check my Lightning wallet balance ``` ### Access an L402 API ``` Get weather data for New York from agent-commerce.store ``` --- ## Project vs Global Config | Location | Scope | Best For | |----------|-------|---------| | `.cursor/mcp.json` | This project only | Sharing tool config with team (no secrets!) | | `~/.cursor/mcp.json` | All projects | API keys, personal tools | When both exist, project-level takes priority. --- The `mcpServers` block is the same one Claude Desktop uses in `claude_desktop_config.json`, so you can copy it between the two. --- ## Next Steps - [Spending Security](/products/agentic-commerce/ai-spending-security) — Budget controls - [MCP Complete Guide](/products/agentic-commerce/mcp-complete-guide) — full tool reference - [Agent Commerce Store](https://agent-commerce.store) — L402 APIs to test with ============================================================================== # Dashboard Guide Source: https://docs.lightningenable.com/products/agentic-commerce/dashboard-guide ============================================================================== # Dashboard Guide Manage L402 proxies, see your endpoints, configure pricing, and set up your account from the browser. No Lightning node or server to run. The navigation has four pages: **Dashboard**, **Proxies**, **Endpoints**, and **Settings**. ## Accessing the Dashboard 1. Navigate to [api.lightningenable.com/dashboard](https://api.lightningenable.com/dashboard) 2. Enter your email for a magic link, or switch to the **API Key** tab and paste your merchant API key. When you open the magic link, type the same email address and press **Sign in** (opening the link alone doesn't sign you in) 3. No password is needed ![Dashboard Home](/img/dashboard/dashboard-home.png) ### Configuring Your Payment Provider Before creating proxies, add your payment provider credentials in **Settings → Payment Provider**: 1. Go to the **Settings** page 2. In the **Payment Provider** section, choose from the **Payment provider** dropdown: **Strike (recommended)**, **OpenNode**, or **Nostr Wallet Connect (your own wallet)** 3. Paste your Strike API key, OpenNode API key, or NWC connection string in the field that appears 4. Click **Save Key** (**Save connection** for NWC). For Strike and OpenNode, click **Validate** to confirm the key works :::tip Get a Strike API Key Sign up at [strike.me](https://strike.me) and get your API key from [dashboard.strike.me](https://dashboard.strike.me). Strike is the recommended provider because it returns preimages, which L402 needs. For NWC, see [Nostr Wallet Connect setup](/nwc-setup/account-setup). ::: ## Dashboard Overview The home screen shows: - **Finish setting up** — shown until your account is ready for production; it names the next required step (for example, adding a payment provider) - **4 summary cards** — Total Proxies, Active Proxies, Total Requests, and Total Revenue (cumulative across all your proxies) - **Quick Actions** — **Create Proxy** and **View All Proxies** - **Recent Proxies** — your most recently modified proxies with status indicators ## Managing Proxies ### Proxy List Navigate to the **Proxies** tab to see all your L402 proxies in a searchable, sortable table. ![Proxy List](/img/dashboard/proxy-list.png) Key features: - **Search** matches against proxy name, Proxy ID slug, and Target URL — type any substring of any of the three - **Sort** columns by Name, Price (sats), Requests, or Revenue - **Enable/disable** proxies with one-click toggle switches - **Status indicators** — green (active), gray (disabled) ### Creating a Proxy Click **Create Proxy**. The page opens in **Smart Setup**: enter your API URL and click **Scan my API's OpenAPI spec**. If a spec is found, Lightning Enable imports every endpoint and pre-fills the manifest details; set a name and price, then click **Create proxy + import endpoints**. If your API has no spec, click **Skip auto-detect and configure manually** for the 4-step wizard: **Step 1: Basic Info** Enter a name and optional description for your proxy. ![Create Proxy - Step 1](/img/dashboard/proxy-create-step1.png) **Step 2: Target** Configure the target API URL — the base URL of the API you are proxying. All requests to `/l402/proxy/{proxy-id}/*` are forwarded to this address. Lightning Enable does not store or inject upstream credentials; see the [proxy setup walkthrough](/products/agentic-commerce/proxy-setup-walkthrough#handling-apis-that-require-authentication) for how to handle APIs that require auth. ![Create Proxy - Step 2](/img/dashboard/proxy-create-step2.png) **Step 3: Pricing** Set the default price in satoshis per request and the token validity period — how long a paid token stays usable before the client must pay again. The USD figure next to the sats price is an **estimate**, not a live market price. ![Create Proxy - Step 3](/img/dashboard/proxy-create-step3.png) **Step 4: Review** Review all settings before creating the proxy. ![Create Proxy - Step 4](/img/dashboard/proxy-create-step4.png) ## Proxy Detail Page Click any proxy to view its detail page with multiple tabs. :::tip Full walkthrough available For a step-by-step guide to the entire setup flow — from creating a proxy to getting discovered by AI agents — see [Setting Up Your Proxy](/products/agentic-commerce/proxy-setup-walkthrough). ::: ### Overview Tab ![Proxy Detail - Overview](/img/dashboard/proxy-detail-overview.png) Shows: - Proxy information (name, Proxy ID, target URL, description, created date) - Statistics (Total Requests and Total Revenue — lifetime totals for this proxy) - Quick Actions — **Activate**/**Deactivate** and **Delete** - **Integration** — the proxy endpoint URL to share with clients and an example request ### Pricing Tab ![Proxy Detail - Pricing](/img/dashboard/proxy-detail-pricing.png) The Pricing tab combines everything needed to make your API discoverable and correctly priced: - **Default price (fallback)** — the price charged for any request that does not match a specific endpoint rule. Set this once if all your endpoints cost the same amount. - **Manifest Settings** — turn on **Make this API visible to agents** to publish the manifest AI agents read, and **List in Public Registry** to appear in registry search. Fill in the Service Description, Contact Email, Documentation URL, and Terms of Service URL. - **API Endpoints table** — the per-endpoint catalog that agents read. Use **Scan API** to import from your OpenAPI/Swagger spec, or **Add Manual** for APIs without a spec. **Test it** on any row mints a live 402 so you can see exactly what an agent receives. A **Price mismatch — fix** chip marks a row whose saved price differs from what the proxy charges. - **Preview & Share** — URLs for the machine-readable manifest (JSON), a human-readable version (Markdown), and the public registry. See the [full Pricing tab walkthrough](/products/agentic-commerce/proxy-setup-walkthrough#step-3--configure-the-manifest) for detailed guidance on each section. Per-day analytics and a request log aren't available yet; the Overview tab shows lifetime totals. ## Endpoints The **Endpoints** page lists every distinct resource you have minted L402 challenges for, whether through a proxy, the [native middleware](/products/agentic-commerce/native-integration), or the [producer API](/products/agentic-commerce/producer-api-reference). Summary cards show **Active Endpoints**, **Challenges This Month**, **Max Price / Challenge**, and your **Plan**, with your plan's limits where it has them. The table shows each resource's status (**Active** or **Retired**), total challenges, this month's challenges, latest price, and when it was last minted. The same data is available from `GET /api/l402/endpoints`. ## Settings - **Account Information** — name, email, plan, payment method, paid-through date, and member-since date - **Appearance** — light or dark mode - **API Credentials** — your Lightning Enable API key, with **Regenerate** to issue a new one (the old key stops working) - **Payment Provider** — see [Configuring Your Payment Provider](#configuring-your-payment-provider) - **Integrations** — connect a Shopify store; see [Shopify Commerce](/products/shopify-commerce/overview) - **Session** — sign out ## Dashboard vs API Both the dashboard and REST API provide full proxy management. Use whichever fits your workflow: | Action | Dashboard | REST API | |--------|-----------|----------| | Create proxy | 4-step visual wizard | `POST /api/proxy` | | Update pricing | Inline editing | `PUT /api/proxy/{proxyId}` | | Toggle status | One-click switch | `PUT /api/proxy/{proxyId}` | | Endpoint pricing | Visual table | `POST /api/proxy/{proxyId}/pricing` | | Test a live 402 | **Test it** on an endpoint row | Call the proxy URL without a credential | | Test target reachability | — | `POST /api/proxy/{proxyId}/test` | | View totals | Overview tab numbers | `GET /api/proxy/{proxyId}/analytics` | | List endpoints | Endpoints page | `GET /api/l402/endpoints` | REST API routes use `{proxyId}`, the URL slug (for example `openweather-current-conditions`), not the numeric id in the dashboard's URL. ## Next Steps - [Proxy Configuration](/products/agentic-commerce/proxy-configuration) - Full API reference for proxy management - [API Monetization](/products/agentic-commerce/api-monetization) - Strategies for monetizing APIs - [How It Works](/products/agentic-commerce/how-it-works) - Technical deep dive into L402 ============================================================================== # Run L402 Anywhere: Hermes + NWC Source: https://docs.lightningenable.com/products/agentic-commerce/hermes-nwc-setup ============================================================================== # Run L402 Anywhere: Hermes + NWC Give an AI agent a tiny Lightning wallet and the Lightning Enable MCP server, and it can pay [L402](https://github.com/lightninglabs/L402)-protected APIs anywhere on the internet — no card, no account, no hosted infrastructure. With [Hermes](https://hermes-agent.nousresearch.com) as the host, the same setup reaches you on your **phone** over Telegram, Signal, WhatsApp, and other chat apps. :::tip Why this setup Most "agent pays for things" demos have a deployment problem: the agent needs a wallet **and** a host before it can pay for anything. Hermes runs the MCP locally and bridges to your phone, so you get mobile agent payments **without** a hosted MCP to run or trust. Your wallet stays yours; Lightning Enable never holds funds. ::: ## The beginner stack | Layer | Pick | Why | |---|---|---| | Agent host | **Hermes** | Local MCP client with Telegram/Signal/WhatsApp gateways — mobile access, no hosted layer. | | Payment tools | **Lightning Enable MCP** | Gives the agent `access_l402_resource`, `pay_l402_challenge`, wallet + budget tools. | | Wallet | **Alby Hub** via NWC | Alby-hosted wallet (Alby Cloud) — no infrastructure of your own to run or trust, and returns the Lightning **preimage** that L402 requires. | | Budget | A small funded wallet | The wallet balance is the hard leash; software limits are defense-in-depth. | :::caution Wallet must return a preimage L402 verification needs the payment **preimage**. NWC wallets (CoinOS, Alby Hub, CLINK), LND, and Strike return it. **OpenNode does not** and can't be used to *pay* L402 resources. ::: ## Prerequisites - macOS, Linux, Windows, or WSL - The .NET 8, 9, or 10 SDK (for the .NET MCP tool) **or** Python 3.10+ (for the Python one) - [Hermes](https://hermes-agent.nousresearch.com) installed - An Alby Hub account (via [Alby Cloud](https://albyhub.com)) with a small amount of sats (1,000–10,000 is plenty to start) ## Step 1 — Install Hermes ```bash curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash ``` Run the setup wizard (pick your model/provider), then confirm it's healthy: ```bash hermes setup hermes doctor ``` ## Step 2 — Install the Lightning Enable MCP ```bash # .NET (recommended) dotnet tool install -g LightningEnable.Mcp # Or Python (NWC support is in the base install): pip install lightning-enable-mcp ``` Confirm the command is on your PATH: ```bash command -v lightning-enable-mcp ``` ## Step 3 — Create an Alby Hub NWC connection 1. Go to [Alby Hub](https://albyhub.com) and choose the **Alby Cloud** hosted option — an Alby-hosted Lightning node, so there's no infrastructure of your own to run or trust. (Already running your own node? Alby Hub can also be self-hosted; Alby Cloud is just the fastest path for this guide.) 2. Fund it with a small amount (e.g. 1,000–10,000 sats). 3. Open **Connections** and create a new **app connection** for your agent. 4. Grant the permissions an L402-paying agent needs: - `pay_invoice` - `get_balance` - (optional) `make_invoice` if the agent should also *receive* payments 5. Set a **monthly budget** for the connection. Alby Hub enforces a real per-connection spend cap here — a genuine advantage over CoinOS, which doesn't offer one. 6. Copy the `nostr+walletconnect://…` connection string. :::warning Treat the NWC string like a wallet password Anyone with the connection string can spend within its permissions and limits. Keep it out of chat, screenshots, and shared configs. ::: ## Step 4 — Configure the wallet + budget Create `~/.lightning-enable/config.json`: ```json { "currency": "USD", "tiers": { "autoApprove": 0.10, "logAndApprove": 1.00, "formConfirm": 10.00 }, "limits": { "maxPerPayment": 5.00, "maxPerSession": 20.00 }, "session": { "requireApprovalForFirstPayment": false }, "wallets": { "nwcConnectionString": "nostr+walletconnect://PASTE_YOUR_ALBY_HUB_STRING_HERE", "priority": "nwc" } } ``` Lock the file down (NWC strings are sensitive): ```bash chmod 600 ~/.lightning-enable/config.json ``` | Threshold | Behavior | |---|---| | Under $0.10 | Auto-pay silently | | $0.10 – $1.00 | Pay and log | | $1.00 – $5.00 | Confirmation required (server prints a code to its console for you) | | Over $5/payment or $20/session | Denied | The table assumes `requireApprovalForFirstPayment` is `false`, as in the sample. If you leave it out, it defaults to `true`, and the first payment of every session needs a code whatever the amount. :::caution Confirmation codes and chat apps The confirmation code appears on the console of the machine running Hermes, not in Telegram or the other chat apps. If you'll talk to the agent only from your phone, set `autoApprove` and `logAndApprove` to cover the payments you expect, keep `maxPerPayment` low, or set `confirmation.channel` to `webhook` so codes reach you elsewhere. See [Confirmation channels](/products/agentic-commerce/ai-spending-security#confirmation-channels). ::: :::note Config file vs. environment variable NWC strings contain characters that are easy to break in shell profiles and GUI launchers, so the config file is the reliable path. (If you prefer env vars, the equivalents are `NWC_CONNECTION_STRING` and `WALLET_PRIORITY=nwc`.) ::: ## Step 5 — Add the MCP to Hermes Because the wallet lives in the config file, adding the server is one line: ```bash hermes mcp add lightning-enable --command lightning-enable-mcp hermes mcp test lightning-enable ``` Expected: ```text ✓ Connected ✓ Tools discovered ``` ## Step 6 — Verify for 1 sat This is the moment of truth — a real end-to-end L402 payment against the public **1-sat test endpoint**. It proves your wallet, the preimage flow, and L402 all work, for one satoshi. First, check the agent can see the wallet: ```text Check the Lightning Enable wallet balance. ``` Then pay the test endpoint: ```text Access this L402 resource: https://api.lightningenable.com/l402/test/ping ``` What happens under the hood: 1. The agent requests the URL and gets `402 Payment Required` with a 1-sat Lightning invoice + macaroon. 2. The MCP pays the invoice through your Alby Hub NWC wallet. 3. Alby Hub returns the preimage. 4. The MCP retries with `Authorization: L402 :`. 5. The endpoint returns `200 OK`. If you get the `200`, your whole stack works. (Prefer the command line? The same endpoint responds to `curl https://api.lightningenable.com/l402/test/ping` with the 402 challenge so you can inspect the invoice before paying.) ## Step 7 — Take it to your phone Point Hermes' messaging gateway at Telegram (or Signal, WhatsApp, etc.) and talk to your agent from anywhere: ```bash hermes gateway setup hermes gateway run ``` Now you can message your agent "pay the 1-sat test endpoint" or "buy the premium forecast from this API" from your phone, and it pays over Lightning from the bounded wallet — no hosted MCP required. ## What the agent can do next Once it can pay L402, the same agent handles real tasks. Pair this setup with the open-source [**pay-l402-anywhere** skill](https://github.com/refined-element/lightning-enable-skills/tree/main/skills/pay-l402-anywhere) (it works in Hermes, Claude Code, and Cursor unmodified) — discover a paid API, check what you can afford, confirm, pay, and report the sats spent. Key consumer tools the MCP exposes: - `access_l402_resource` — fetch a URL and auto-pay the L402 challenge - `pay_l402_challenge` — pay a challenge you already hold - `discover_api` — search the L402 registry or fetch an API's manifest - `get_balance` / `budget` (with `action="status"`) — know your funds and limits - `verify_confirmation_code` — check what a console confirmation code authorizes (verification only; to approve an over-threshold payment, re-call the original payment tool with `confirmationNonce` / `confirmation_nonce`) ## Security posture The safe design isn't "trust the agent" — it's **bound what it can spend**: 1. Use a **dedicated small wallet** for the agent. 2. Fund only what it's allowed to spend — the balance is the real ceiling. 3. Set an Alby Hub per-connection monthly budget — a real spend cap Alby Hub enforces on that connection alone. 4. Keep the MCP budget caps as defense-in-depth. 5. Keep `~/.lightning-enable/config.json` at `600`. 6. Rotate the NWC connection if it leaks or you're done with it. A 5,000-sat wallet cannot spend 50,000 sats, no matter what any prompt says. ## Next steps - [Wallet Configuration](/products/agentic-commerce/mcp-wallet-setup) — other NWC wallets (CoinOS, CLINK) and LND - [AI Spending Security](/products/agentic-commerce/ai-spending-security) — how the budget tiers and confirmation gate work - [MCP Complete Guide](/products/agentic-commerce/mcp-complete-guide) — full tool reference ============================================================================== # How It Works Source: https://docs.lightningenable.com/products/agentic-commerce/how-it-works ============================================================================== # How L402 Works This guide explains the technical details of the L402 protocol implementation in Lightning Enable. ## Protocol Flow ``` ┌──────────┐ ┌─────────────────┐ ┌─────────────┐ │ Client │ │ Lightning Enable│ │ Provider │ └────┬─────┘ └────────┬────────┘ └──────┬──────┘ │ │ │ │ 1. GET /api/premium/data │ │ │──────────────────────────────────>│ │ │ │ │ │ │ 2. Create Lightning Invoice │ │ │───────────────────────────────────>│ │ │ │ │ │ 3. Invoice + Payment Hash │ │ │<───────────────────────────────────│ │ │ │ │ 4. HTTP 402 Payment Required │ │ │ WWW-Authenticate: L402 │ │ │ macaroon="...", invoice="..." │ │ │<──────────────────────────────────│ │ │ │ │ │ 5. Pay invoice (via any wallet) │ │ │─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─>│ │ │ │ │ 6. Preimage (proof of payment) │ │ │<─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─│ │ │ │ │ 7. GET /api/premium/data │ │ │ Authorization: L402 mac:preim │ │ │──────────────────────────────────>│ │ │ │ │ │ │ 8. Verify SHA256(preimage)==hash │ │ │ Verify macaroon signature │ │ │ │ │ 9. HTTP 200 OK (response) │ │ │<──────────────────────────────────│ │ ``` *Provider* is the merchant's configured payment provider: **Strike** (the default), **OpenNode**, or the merchant's own wallet over **Nostr Wallet Connect**. The L402 flow is identical either way. Lightning Enable does not hold funds; the payment provider facilitates custody and settlement. ## Key Concepts ### 1. Lightning Invoice (BOLT11) When a client requests a protected endpoint, Lightning Enable creates a Lightning invoice: ``` lnbc100n1pnxyzabc... (encoded invoice) ``` The invoice contains: - **Amount** in satoshis - **Payment hash** (SHA256 of a secret preimage) - **Expiry time** - **Destination** (the payment provider's node, or the merchant's wallet on NWC) ### 2. Payment Hash & Preimage The payment hash is the key to L402: ``` preimage (32 bytes, secret) → SHA256 → payment_hash (32 bytes, public) ``` - The **payment hash** is included in the invoice - The **preimage** is revealed when the invoice is paid - Knowing the preimage proves payment was made ### 3. Macaroon A macaroon is a cryptographic bearer token signed with HMAC-SHA256. Lightning Enable embeds a set of **caveats** (restrictions) into every macaroon at issuance time. These caveats bind the token to the exact context it was created for: ```json { "identifier": "lightning-enable:payment_hash:expires", "caveats": [ "services = lightning-enable:0", "path = /api/premium/data", "merchant_id = 42", "charge_id = abc123-def456", "amount_sats = 100", "expires = 1704067200" ], "signature": "hmac-sha256-signature" } ``` Each caveat enforces a specific security constraint: | Caveat | Purpose | |--------|---------| | `path` | Binds the token to the API path it was issued for. Supports exact match or wildcard prefix (e.g., `/l402/proxy/my-api/*`). | | `merchant_id` | Binds the token to the issuing merchant, preventing cross-tenant token reuse. | | `amount_sats` | Binds the token to the price at issuance, preventing reuse at a different price tier. | | `expires` | Sets the token expiration as a Unix timestamp (default: 1 hour). | | `charge_id` | Records the payment provider's charge ID for the associated payment. | | `services` | Identifies the service name and tier. | | `method` | Proxy only, when present: binds the token to one HTTP method. | All caveats are verified on every request. Any unrecognized caveat causes verification to fail (closed-world), so a token can never carry a restriction the server silently ignores. ### 4. L402 Credential The client combines macaroon and preimage: ``` Authorization: L402 : ``` Lightning Enable also accepts the `Payment` (MPP) authorization scheme for the same invoice. See [Payment (MPP) credentials](/api-reference/l402#payment-mpp-credentials). ## Verification Process When Lightning Enable receives an L402 credential, it: 1. Splits the credential into macaroon and preimage. 2. Checks that `SHA256(preimage)` equals the payment hash in the macaroon. 3. Verifies the macaroon's HMAC signature against the root key. 4. Checks every caveat: not expired, path matches, merchant matches, price matches, and method matches when bound. Any failure produces a fresh 402 (see [Failed verification](#failed-verification--a-fresh-402-never-401403) below). ## Multi-Use Tokens A single L402 payment can be used for multiple requests during the token validity period: 1. Client pays once 2. Receives preimage 3. Uses same macaroon:preimage for subsequent requests 4. Token valid until expiration — configurable per proxy (10 minutes to 24 hours), 1 hour by default ## Security Considerations ### Caveat-Based Token Binding Macaroon caveats are the primary defense against token misuse. Lightning Enable enforces caveats that prevent three categories of attack: **Path binding** (`path` caveat) -- A token issued for `/api/premium/v1` cannot be used to access `/api/premium/v2`. This prevents clients from paying for a cheap endpoint and reusing the token against an expensive one. Wildcard paths (e.g., `/l402/proxy/my-api/*`) allow sub-path access when appropriate. **Merchant isolation** (`merchant_id` caveat) -- In Lightning Enable's multi-tenant architecture, each merchant operates independently. The `merchant_id` caveat prevents a token issued by Merchant A from being replayed against Merchant B's endpoints. This is enforced bidirectionally: if a request carries a merchant context, the token must contain a matching `merchant_id`, and if a token contains a `merchant_id`, the request must have a matching merchant context. **Price tier enforcement** (`amount_sats` caveat) -- A token purchased at 10 sats for a demo endpoint cannot be reused against a 100-sat premium endpoint, even if both endpoints share a wildcard path pattern. The server compares the token's `amount_sats` caveat against the current endpoint's configured price and rejects mismatches. **Unknown caveat rejection** -- Any caveat the server does not recognize causes verification to fail. This closed-world approach ensures that if new caveat types are added in the future, older verification logic will not silently skip them. ### Preimage Security - Treat preimages like passwords - Don't log full preimages - Use HTTPS to prevent interception ### Macaroon Tampering - Macaroons are signed with HMAC-SHA256 - Root key must be kept secret (`L402_ROOT_KEY` environment variable) - Any modification invalidates the signature ### Token Expiration - Configure appropriate validity periods - Shorter = more secure, but more payments needed - Longer = better UX, but higher risk if compromised ### Rate Limiting A paid token is reusable until it expires. If an endpoint is expensive to serve, rate-limit per payment hash in your own API (native mode), or keep the token validity short. ### Defaults | Setting | Default | Who sets it | |---|---|---| | Invoice expiry | 10 minutes | Lightning Enable | | Token validity | 1 hour | You, per proxy (10 minutes to 24 hours) | | Price | Per proxy, endpoint rule, or challenge | You, in the [dashboard](/products/agentic-commerce/dashboard-guide) or the [proxy API](/products/agentic-commerce/proxy-configuration) | ## Error Responses ### 402 Payment Required (initial challenge) ```json { "error": "Payment Required", "message": "Pay the Lightning invoice to access this resource", "l402": { "macaroon": "AgEL...", "invoice": "lnbc100n1p3...", "amount_sats": 100, "payment_hash": "abc123...", "expires_at": "2026-07-03T13:00:00Z" } } ``` ### Failed verification → a fresh 402, never 401/403 When a presented L402 credential fails verification — malformed credential, preimage/payment-hash mismatch, invalid macaroon signature, expired token, or a caveat violation (wrong path, merchant, or price tier) — Lightning Enable does **not** return `401 Unauthorized` or `403 Forbidden`. It **re-issues a fresh 402 challenge** with a new invoice and macaroon. The failure reason is carried in: - the **`X-L402-Error` response header**, and - the body's **`message`** field. ``` HTTP/1.1 402 Payment Required WWW-Authenticate: L402 macaroon="AgEL...", invoice="lnbc100n1p3..." X-L402-Error: Macaroon has expired ``` ```json { "error": "Payment Required", "message": "Macaroon has expired", "l402": { "macaroon": "AgEL... (new macaroon)", "invoice": "lnbc100n1p3... (new invoice)", "amount_sats": 100, "payment_hash": "def456...", "expires_at": "2026-07-03T13:10:00Z" } } ``` Clients should treat **every** 402 as a (re-)challenge: check `X-L402-Error` to learn why the previous credential was rejected, then pay the new invoice from the current response. Code that branches on 401/403 for L402 failures will never execute those branches. ## Next Steps - [API Monetization](/products/agentic-commerce/api-monetization) - Protect your endpoints - [Proxy Configuration](/products/agentic-commerce/proxy-configuration) - Monetize any API - [API Reference](/api-reference/l402) - Complete L402 API docs ============================================================================== # L402 Producer API: Agents That Earn Source: https://docs.lightningenable.com/products/agentic-commerce/l402-producer-api ============================================================================== # L402 Producer API: Agents That Earn :::tip The Key Differentiator Until now, AI agents could only **spend** money. With the L402 Producer API, your agents can **earn** money too. This is true agentic commerce — agents that both buy and sell services autonomously. ::: The L402 Producer API lets your AI agents act as service providers. Instead of just consuming paid APIs, your agent can **create L402 payment challenges** and **verify payments** before granting access to its own capabilities. ## The Two Sides of Agentic Commerce | Side | What It Does | Who Pays | MCP Tools | |------|-------------|----------|-----------| | **Consumer** (existing) | Agent accesses paid APIs | Your agent pays | `access_l402_resource`, `pay_l402_challenge` | | **Producer** (new) | Agent charges for its services | Other agents pay you | `l402_producer` with `action="create"` / `action="verify"` (formerly the separate `create_l402_challenge` / `verify_l402_payment` tools — see [Deprecated aliases](/products/agentic-commerce/mcp-complete-guide#deprecated-aliases)) | Both sides use the same L402 protocol. The difference is direction: consumers pay invoices, producers create them. ## Prerequisites - **A Lightning Enable account** — the free Free Producer Sandbox (3 endpoints, 200 challenges/month, 1,000 sats max per challenge) works; Agentic Commerce ($49/mo) or Business ([contact us](mailto:support@lightningenable.com)) removes the caps - **`LIGHTNING_ENABLE_API_KEY`** environment variable set to your merchant API key - A configured payment provider (Strike, OpenNode, or an NWC wallet) on your Lightning Enable account - The MCP server, installed with `dotnet tool install -g LightningEnable.Mcp` (puts the `lightning-enable-mcp` command on your PATH) ```json { "mcpServers": { "lightning-enable": { "command": "lightning-enable-mcp", "env": { "STRIKE_API_KEY": "your-strike-api-key", "LIGHTNING_ENABLE_API_KEY": "your-merchant-api-key" } } } } ``` :::info Consumer Tools Work Out of the Box The out-of-the-box tools (`access_l402_resource`, `pay_l402_challenge`, `pay_invoice`, etc.) require **no API key** — just a wallet. `l402_producer` (every action) and four `agent_services` actions (`request`, `publish`, `unpublish`, `attest`) require a Lightning Enable API key via `LIGHTNING_ENABLE_API_KEY`; the other `agent_services` actions (`discover`, `settle`, `reputation`) work with just a wallet. See the [MCP Complete Guide](/products/agentic-commerce/mcp-complete-guide) for the full tool list. ::: ## How It Works The full agent-to-agent commerce flow: ``` ┌──────────────────┐ ┌──────────────────┐ │ Requesting Agent │ │ Producer Agent │ │ (Consumer) │ │ (Your Agent) │ └────────┬─────────┘ └────────┬─────────┘ │ │ │ 1. "I need weather data" │ │──────────────────────────────────────> │ │ │ │ │ 2. create_l402_challenge( │ │ resource="/api/weather", │ │ priceSats=50, │ │ description="7-day forecast" │ │ ) │ │ │ 3. HTTP 402 Payment Required │ │ + Lightning invoice + macaroon │ │ <──────────────────────────────────────│ │ │ │ 4. pay_l402_challenge(invoice, mac) │ │ ─ ─ ─ ─ ─ ─ ─ (pays invoice) ─ ─ ─ >│ │ │ │ 5. "Here's my L402 token: │ │ macaroon:preimage" │ │──────────────────────────────────────> │ │ │ │ │ 6. verify_l402_payment( │ │ macaroon, preimage │ │ ) │ │ │ 7. Access granted + response data │ │ <──────────────────────────────────────│ ``` *(Steps 2 and 6 are shorthand for `l402_producer` with `action="create"` and `action="verify"` — see below.)* ## MCP Tools :::note Now one consolidated tool `create_l402_challenge` and `verify_l402_payment` are consolidated into `l402_producer` with `action="create"` / `action="verify"` — one tool, selected by action, alongside the newer `configure_receive`/`status`/`create_proxy`/`add_endpoint`/`publish`/`list_challenges` actions covered in [Sell With Your Agent](/getting-started/sell-with-your-agent) and the [MCP Complete Guide](/products/agentic-commerce/mcp-complete-guide). The old tool names still dispatch as deprecated aliases under every profile until v3.0.0, and `LIGHTNING_ENABLE_TOOL_PROFILE=full` also lists them. The examples below use the consolidated call form. ::: ### l402_producer with action="create" Create an L402 payment challenge to charge another agent or user for accessing a resource. Formerly the standalone `create_l402_challenge` tool. **Parameters:** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `resource` | string | Yes | Resource identifier — URL, service name, or description of what you're charging for | | `priceSats` | long | Yes | Price in satoshis to charge | | `description` | string | No | Description shown on the Lightning invoice | **Example:** ``` User: When another agent asks for weather data, charge them 50 sats. Claude: [Uses l402_producer with action="create", resource="/api/weather/forecast", priceSats=50, description="7-day weather forecast"] L402 challenge created! - Invoice: lnbc500n1p3xyza... - Price: 50 sats - Resource: /api/weather/forecast Share the invoice and macaroon with the requesting agent. After they pay, they'll send you an L402 token (macaroon:preimage). Use l402_producer with action="verify" to confirm payment before granting access. ``` **Response:** ```json { "success": true, "challenge": { "invoice": "lnbc500n1p3xyza...", "macaroon": "AgELbGlnaHRuaW5n...", "paymentHash": "abc123def456...", "expiresAt": "2026-03-13T14:30:00Z" }, "resource": "/api/weather/forecast", "priceSats": 50, "instructions": { "forPayer": "Pay the Lightning invoice, then present the L402 token...", "tokenFormat": "L402 {macaroon}:{preimage}", "verifyWith": "After receiving the L402 token from the payer, use l402_producer with action=\"verify\" to confirm payment before granting access." }, "message": "L402 challenge created for 50 sats. Share the invoice with the payer." } ``` --- ### l402_producer with action="verify" Verify an L402 token (macaroon + preimage) to confirm payment was made. Use this after a payer presents an L402 token, before granting access to the resource. Formerly the standalone `verify_l402_payment` tool. **Parameters:** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `macaroon` | string | Yes | Base64-encoded macaroon from the L402 token | | `preimage` | string | Yes | Hex-encoded preimage (proof of payment) | :::caution Compare the returned resource yourself This tool sends only the macaroon and preimage to the verify endpoint. The API always enforces merchant binding and expiry, but it enforces the **resource** caveat only when the verify request includes a `resource` value — which this tool does not send. `valid: true` therefore means "a real payment for *some* resource of yours" — check that the `resource` field in the result matches the resource the payer is asking for before granting access. See [Token Binding](#token-binding) below. ::: **Example:** ``` User: The requesting agent sent this L402 token. Verify it. Claude: [Uses l402_producer with action="verify", macaroon="AgEL...", preimage="7f8a9b..."] Payment verified! The agent has paid 50 sats for /api/weather/forecast. Granting access now. ``` **Response (valid):** ```json { "success": true, "valid": true, "resource": "/api/weather/forecast", "message": "Payment verified. The payer has paid — you can now grant access to the resource." } ``` **Response (invalid):** ```json { "success": true, "valid": false, "message": "Payment verification failed. The token is invalid or the invoice has not been paid. Do NOT grant access." } ``` ## Challenge Idempotency Send an `Idempotency-Key` header on every mint. A retry then gets the **same invoice and macaroon** back for the life of that invoice, never a second one your payer could also pay, even across a deploy or restart. Derive the key from the work you're serving (an order id or request id), not a fresh UUID per attempt. ```bash curl -X POST https://api.lightningenable.com/api/l402/challenges \ -H "X-API-Key: YOUR_MERCHANT_API_KEY" \ -H "Idempotency-Key: forecast-req-7f2a" \ -H "Content-Type: application/json" \ -d '{"resource": "/api/weather/forecast", "priceSats": 50}' ``` The same key with a different resource or price, while the invoice is live, is a `409`. Without a key, every call mints a fresh challenge. Full rules: [Idempotency](./producer-api-reference#idempotency). ## Knowing When You Got Paid **Poll** — `GET /api/l402/challenges?status=paid` lists what you have minted with a `status` of `paid`, `unpaid`, or `expired`. **Get notified** — set a callback URL with [`PUT /api/merchant/webhook-url`](/api-reference/merchant-settings#update-webhook-url) and Lightning Enable POSTs `l402.challenge.paid` (with `paymentHash`, `resource`, `amountSats`, `paidAt`, and `idempotencyKey`) the first time a credential from one of your challenges verifies. It fires once per challenge. It is proof of payment, not a settlement record: Lightning Enable does not hold funds, and the sats settled with your payment provider when the invoice was paid. Details: [Listing challenges](./producer-api-reference#get-apil402challenges) and [Payment webhooks](./producer-api-reference#payment-webhooks). ## REST API Quick Start The MCP tools call two REST endpoints under the hood. You can call them directly from any language. :::info Single source of truth The **[Producer API Reference](./producer-api-reference)** is the authoritative page for these endpoints — full request/response contracts (including the `mppChallenge` field), error tables, idempotency semantics, and the caveat-enforcement rules. The two calls below are just the quick start. ::: Create a challenge: ```bash curl -X POST https://api.lightningenable.com/api/l402/challenges \ -H "X-API-Key: YOUR_MERCHANT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "resource": "/api/weather/forecast", "priceSats": 50, "description": "7-day weather forecast" }' ``` Verify a token — pass `resource` so the API enforces the macaroon's path caveat server-side (omit it and the bound resource is only reported back, not compared): ```bash curl -X POST https://api.lightningenable.com/api/l402/challenges/verify \ -H "X-API-Key: YOUR_MERCHANT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "macaroon": "AgELbGlnaHRuaW5n...", "preimage": "7f8a9b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a", "resource": "/api/weather/forecast" }' ``` The verify endpoint returns **200 OK for both valid and invalid tokens** — read the `valid` field, not the status code. ## End-to-End Example: Weather Data Agent Here is a complete example of a producer agent that charges for weather data. ### Producer Agent Setup ```json { "mcpServers": { "lightning-enable": { "command": "lightning-enable-mcp", "env": { "STRIKE_API_KEY": "your-strike-key", "LIGHTNING_ENABLE_API_KEY": "your-merchant-api-key" } } } } ``` ### Producer Agent Behavior ``` Requesting Agent: "I need the 7-day forecast for New York City." Producer Agent (Claude): 1. [Uses l402_producer with action="create", resource="/api/weather/forecast/nyc", priceSats=25, description="7-day NYC weather forecast" ] 2. Returns to requesting agent: "Access to this forecast costs 25 sats. Pay this invoice: lnbc250n1p3... Then send me: L402 AgEL...:your_preimage" Requesting Agent: 3. [Uses pay_l402_challenge(invoice="lnbc250n1p3...", macaroon="AgEL...")] 4. "Here's my token: L402 AgEL...:7f8a9b2c..." Producer Agent (Claude): 5. [Uses l402_producer with action="verify", macaroon="AgEL...", preimage="7f8a9b2c..."] 6. Payment verified! Now fetching the forecast... 7. "NYC 7-Day Forecast: Monday: Sunny, 72F Tuesday: Partly cloudy, 68F ..." ``` ### Consumer Agent Setup The consumer agent only needs a wallet — no subscription required: ```json { "mcpServers": { "lightning-enable": { "command": "lightning-enable-mcp", "env": { "STRIKE_API_KEY": "consumer-strike-key" } } } } ``` ## Use Cases The call is the same for any service; only `resource`, `priceSats`, and `description` change. For example, a research agent selling a report (`/research/market-analysis`, 100 sats), a code-review agent (`/services/code-review`, 500 sats), a data agent (`/data/crypto-sentiment`, 50 sats), or a translation agent (`/translate/en-to-ja`, 10 sats). ## Security Considerations ### Always Verify Before Granting Access Never grant access based on a payer claiming they paid. Always call `l402_producer` with `action="verify"` to cryptographically confirm: - The preimage matches the payment hash (`SHA256(preimage) == payment_hash`) - The macaroon signature is valid (not tampered with) - The macaroon was issued under **your** merchant account (always enforced server-side) - The token has not expired (always enforced server-side) ### Token Binding Merchant binding and expiry (60 minutes by default) are always enforced. The resource and amount are enforced only when the verify request includes `resource` and `amountSats`. The MCP `action="verify"` sends neither, so compare the `resource` in the tool result against what you're about to grant. Calling the REST endpoint directly, pass `resource` (and `amountSats` if you gate several prices). See [caveat enforcement rules](./producer-api-reference#token-reuse-within-the-validity-window). ## Next Steps - [Sell With Your Agent](/getting-started/sell-with-your-agent) — the same producer flow driven end to end by an MCP agent, from an empty account to a live paid endpoint - [MCP Complete Guide](/products/agentic-commerce/mcp-complete-guide) — full tool reference - [API Monetization](/products/agentic-commerce/api-monetization) — Monetize existing APIs via native middleware (recommended) or hosted proxy mode - [AI Agent Integration](/products/agentic-commerce/ai-agent-integration) — Consumer-side L402 tools and wallet setup - [L402 API Reference](/api-reference/l402) — Complete L402 protocol reference ============================================================================== # LND Node Setup Source: https://docs.lightningenable.com/products/agentic-commerce/lnd-setup ============================================================================== # LND Node Setup Guide This guide covers connecting the Lightning Enable MCP server to your own LND (Lightning Network Daemon) node. **LND always returns the preimage**, making it the most reliable option for L402 authentication. ## Why Use Your Own LND Node? | Benefit | Description | |---------|-------------| | **Guaranteed L402** | LND always returns preimage - L402 never fails due to wallet limitations | | **You Hold the Keys** | You run the node — you control your funds, not a third party | | **No API Limits** | No provider rate limits or account restrictions | | **No Third Party** | No provider account sits between you and your funds | :::tip Best For LND is ideal for: - Power users who already run Lightning nodes - Businesses that need 100% L402 reliability - Users who want to run the node and hold the keys themselves - Developers testing L402 implementations ::: --- ## Prerequisites ### You Need 1. **A running LND node** with: - REST API enabled (default port 8080) - Funded channels with outbound liquidity - Admin macaroon access 2. **Network access** from your MCP server to LND: - Same machine: `localhost:8080` - Local network: `192.168.x.x:8080` - Remote: use a VPN ### LND Installation Options If you don't have LND yet: | Platform | Recommended Approach | |----------|---------------------| | **Start9** | Built-in LND package | | **Umbrel** | Lightning app from store | | **RaspiBlitz** | Included by default | | **Voltage** | Cloud-hosted LND | | **Manual** | [LND Installation Guide](https://docs.lightning.engineering/lightning-network-tools/lnd/installation) | --- ## Step 1: Enable LND REST API LND's REST API is enabled by default on port 8080. Verify in your `lnd.conf`: ```ini [Application Options] # REST API (enabled by default) restlisten=0.0.0.0:8080 # TLS settings tlsextraip=0.0.0.0 tlsextradomain=your-domain.com ``` If you changed the port, note it for configuration. ### Restart LND After Config Changes ```bash # Stop LND lncli stop # Start LND (method depends on your setup) lnd # or systemctl restart lnd ``` --- ## Step 2: Get Your Admin Macaroon The macaroon is your authentication credential. You need the **admin macaroon** for payments. ### Location by Platform | Platform | Default Path | |----------|-------------| | **Linux** | `~/.lnd/data/chain/bitcoin/mainnet/admin.macaroon` | | **macOS** | `~/Library/Application Support/Lnd/data/chain/bitcoin/mainnet/admin.macaroon` | | **Windows** | `%LOCALAPPDATA%\Lnd\data\chain\bitcoin\mainnet\admin.macaroon` | | **Start9** | SSH in, check `/embassy-data/package-data/volumes/lnd/` | | **Umbrel** | `~/umbrel/app-data/lightning/data/lnd/data/chain/bitcoin/mainnet/admin.macaroon` | | **Docker** | Inside container at `/root/.lnd/data/chain/bitcoin/mainnet/admin.macaroon` | ### Convert Macaroon to Hex The MCP server needs the macaroon in **hex format** (not base64). **Linux/macOS:** ```bash xxd -ps -c 1000 ~/.lnd/data/chain/bitcoin/mainnet/admin.macaroon ``` **Windows PowerShell:** ```powershell [System.BitConverter]::ToString([System.IO.File]::ReadAllBytes("$env:LOCALAPPDATA\Lnd\data\chain\bitcoin\mainnet\admin.macaroon")) -replace '-','' ``` **Docker:** ```bash docker exec lnd xxd -ps -c 1000 /root/.lnd/data/chain/bitcoin/mainnet/admin.macaroon ``` The output looks like: ``` 0201036c6e6402f801030a10b3b1a4d3e5f6789012345678901234561201... ``` Save this string - you'll need it for configuration. --- ## Step 3: Test LND Connection Before configuring the MCP server, verify LND is accessible: ```bash # Test from the machine running MCP curl --insecure \ -H "Grpc-Metadata-macaroon: YOUR_MACAROON_HEX" \ https://localhost:8080/v1/getinfo ``` You should see JSON output with your node info: ```json { "identity_pubkey": "03abc...", "alias": "MyNode", "num_active_channels": 5, ... } ``` ### Common Test Issues | Error | Solution | |-------|----------| | `Connection refused` | LND not running or REST not enabled | | `certificate unknown` | Use `--insecure` for this test; the MCP server needs `LND_TLS_CERT_PATH` (Step 4) | | `permission denied` | Wrong macaroon or insufficient permissions | | `timeout` | Firewall blocking port 8080 | --- ## Step 4: Configure MCP Server The `"command": "lightning-enable-mcp"` configurations on this page run the .NET global tool. Install it first with `dotnet tool install -g LightningEnable.Mcp`, which puts the `lightning-enable-mcp` command on your PATH. LND serves REST over HTTPS with a self-signed certificate, so the MCP server must be told to trust it, even on `localhost`. Copy the node's `tls.cert` (for example `~/.lnd/tls.cert`) to the machine running the MCP server and set `LND_TLS_CERT_PATH` to its path. `LND_TLS_CERT_PATH` is an environment variable only; there is no config-file key for it. ### Option A: Environment Variables ```json { "mcpServers": { "lightning-enable": { "command": "lightning-enable-mcp", "env": { "LND_REST_HOST": "localhost:8080", "LND_MACAROON_HEX": "0201036c6e6402f801030a10...", "LND_TLS_CERT_PATH": "/home/you/.lnd/tls.cert" } } } } ``` ### Option B: Config File Add a `wallets` block to `~/.lightning-enable/config.json` (the server creates the file on first run; keep its budget settings), and still set `LND_TLS_CERT_PATH` in the MCP client's `env`: ```json { "wallets": { "lndRestHost": "localhost:8080", "lndMacaroonHex": "0201036c6e6402f801030a10b3b1a4d3e5f6789012345678901234561201...", "priority": "lnd" } } ``` Environment variables override the config file. ### Configuration Options | Variable | Config Key | Description | Required | |----------|-----------|-------------|----------| | `LND_REST_HOST` | `lndRestHost` | LND REST host and port (`https://` is assumed) | Yes | | `LND_MACAROON_HEX` | `lndMacaroonHex` | Macaroon in hex | Yes | | `LND_TLS_CERT_PATH` | - | Path to the node's `tls.cert` (PEM or DER) | For a self-signed certificate | | `LND_SKIP_TLS_VERIFY` | - | `true` turns off certificate checks (development only) | No | | `LND_PAYMENT_TIMEOUT_SECONDS` | - | How long a payment may stay in flight before it is reported as pending (default 25) | No | | `LND_FEE_LIMIT_SATS` | - | Fixed routing-fee ceiling per payment (default 5% of the invoice, minimum 2 sats) | No | | `WALLET_PRIORITY` | `priority` | Set to `lnd` when several wallets are configured | Optional | --- ## Step 5: Verify Connection Restart your MCP client and check the MCP server's stderr output: ``` [LND] Initialized REST client for localhost:8080 Using LND wallet backend LND always returns preimage - L402 fully supported ``` Test with a balance check: ``` User: Check my Lightning balance Claude: [Uses get_balance] Your LND balance: 500,000 sats ``` --- ## Remote LND Access ### Same Local Network ```json { "wallets": { "lndRestHost": "192.168.1.100:8080" } } ``` ### Over VPN 1. Connect both machines to same VPN 2. Use VPN IP address: ```json { "wallets": { "lndRestHost": "10.8.0.1:8080" } } ``` ### TLS Certificate Handling For remote connections, you may need to handle TLS: **Option 1: Skip TLS verification (development only)** Set environment variable: ``` LND_SKIP_TLS_VERIFY=true ``` **Option 2: Pin the TLS cert (recommended)** Copy `~/.lnd/tls.cert` to the MCP machine and set: ``` LND_TLS_CERT_PATH=/path/to/tls.cert ``` If both are set, the pinned certificate wins. --- ## Available Tools with LND | Tool | Description | Works with LND | |------|-------------|----------------| | `pay_invoice` | Pay any Lightning invoice | ✅ Yes (with preimage) | | `get_balance` | View channel balance (BTC only on LND) | ✅ Yes | | `create_invoice` | Create invoice to receive | ✅ Yes | | `check_invoice_status` | Check if invoice paid | ✅ Yes | | `wallet_ops` (`action="send_onchain"`) | Send on-chain Bitcoin | ✅ Yes | | `access_l402_resource` | L402 auto-pay | ✅ Yes | | `pay_l402_challenge` | Manual L402 payment | ✅ Yes | ### Tools NOT Available with LND - `wallet_ops` with `action="price"` - Use Strike for price data - `wallet_ops` with `action="exchange"` - LND is BTC-only --- ## Troubleshooting ### "Failed to connect to LND" **Check LND is running:** ```bash lncli getinfo ``` **Check REST port is open:** ```bash netstat -tlnp | grep 8080 ``` **Check firewall:** ```bash # Linux sudo ufw status # or sudo iptables -L # Allow port 8080 sudo ufw allow 8080 ``` ### "Permission denied" or "Macaroon invalid" **Verify macaroon is correct:** ```bash # Re-export macaroon xxd -ps -c 1000 ~/.lnd/data/chain/bitcoin/mainnet/admin.macaroon ``` **Check macaroon permissions:** the MCP server calls `routerrpc.Router/SendPaymentV2` (pay), `ChannelBalance` (balance), `AddInvoice` and `LookupInvoice` (invoices), and `SendCoins` (on-chain sends). A custom macaroon must allow each one you use; see [Limit Macaroon Scope](#2-limit-macaroon-scope). ### "No route found" / "FAILURE_REASON_NO_ROUTE" Your node can't find a path to the destination: 1. **Check outbound liquidity:** ```bash lncli listchannels | jq '.channels[] | {alias: .peer_alias, local: .local_balance, remote: .remote_balance}' ``` 2. **Open new channels** to well-connected nodes 3. **Wait for channel confirmation** if recently opened ### "Invoice expired" The L402 invoice expired before payment completed. This can happen with: - Slow network connections - Large payments requiring route finding Try again - the API will issue a fresh invoice. ### "TLS handshake failed" **For self-signed certs (development):** ```json { "env": { "LND_SKIP_TLS_VERIFY": "true" } } ``` **For production:** Copy your `tls.cert` to the MCP server machine and set `LND_TLS_CERT_PATH` to its path. --- ## Security Best Practices ### 1. Use a Dedicated Node or Small Channels The macaroon can spend whatever the node can route. Fund the node (or the channels the agent uses) with only what you are willing to let the agent spend. ### 2. Limit Macaroon Scope Instead of the admin macaroon, bake one that can only pay and read balances: ```bash lncli bakemacaroon --save_to=./payment.macaroon \ uri:/routerrpc.Router/SendPaymentV2 \ uri:/lnrpc.Lightning/ChannelBalance ``` Add `uri:/lnrpc.Lightning/AddInvoice uri:/lnrpc.Lightning/LookupInvoice` if the agent should create and check invoices, and `uri:/lnrpc.Lightning/SendCoins` for on-chain sends. Convert it to hex the same way as the admin macaroon. ### 3. Use a VPN for Remote Access Never expose the LND REST API directly to the internet. Reach a remote node over a VPN. ### 4. Set Payment Limits in Config The MCP config file provides additional spending controls: ```json { "limits": { "maxPerPayment": 100.00, "maxPerSession": 50.00 } } ``` --- ## Performance Tips ### Channel Management For best L402 performance: - Maintain 5+ active channels - Keep channels balanced (50% local / 50% remote) - Open channels to well-connected LSPs (Lightning Service Providers) ### Monitoring Use `lncli` to monitor your node: ```bash # Check pending payments lncli listpayments --max_payments 10 # Check channel health lncli listchannels --active_only # Check forwards (routing) lncli fwdinghistory ``` --- ## Frequently Asked Questions ### Can I use LND on a Raspberry Pi? Yes, but ensure adequate resources: - 4GB+ RAM - SSD storage (not SD card) - Stable internet connection ### Does this work with Core Lightning (CLN)? Not yet. LND is currently the only supported node implementation. CLN support may be added in the future. ### Can I use a hosted LND (Voltage, etc.)? Yes! Voltage and similar services provide the REST API and macaroon. Configure as remote connection. ### What if my node is offline? Payments will fail. Ensure your node has reliable uptime for production use. ### Is my macaroon safe in the config file? The config file is stored locally on your machine. Keep it protected like any credential file: - Don't commit to git - Use file permissions (chmod 600) - Consider encrypted storage --- ## Next Steps - [MCP Complete Guide](/products/agentic-commerce/mcp-complete-guide) - Using L402 tools - [AI Spending Security](/products/agentic-commerce/ai-spending-security) - Budget configuration - [Wallet Configuration](/products/agentic-commerce/mcp-wallet-setup) - Other wallet options ============================================================================== # MCP Complete Guide Source: https://docs.lightningenable.com/products/agentic-commerce/mcp-complete-guide ============================================================================== # Lightning Enable MCP - Complete Guide The Lightning Enable MCP (Model Context Protocol) server lets an AI agent pay, receive, and sell over Lightning. It is free and open source (MIT). This page is the full reference for its tools, configuration, and spending controls. For a 3-minute setup, see [MCP Quick Start](/products/agentic-commerce/mcp-quickstart); for wallet-specific steps, see [Wallet Configuration](/products/agentic-commerce/mcp-wallet-setup). :::info Open Source The MCP server is fully open source: [github.com/refined-element/lightning-enable-mcp](https://github.com/refined-element/lightning-enable-mcp). Available on NuGet, PyPI, and Docker. ::: ## Overview ``` ┌──────────────────────────────────────────────────────────────────────────┐ │ Lightning Enable MCP │ ├──────────────────────────────────────────────────────────────────────────┤ │ AI Agent (Claude, etc.) │ │ ↓ │ │ MCP Server (wallet priority: LND > NWC > Strike > OpenNode) │ │ ↓ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ LND │ │ NWC │ │ Strike │ │ OpenNode │ │ │ │ Wallet │ │ Wallet │ │ Wallet │ │ Wallet │ │ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │ ↓ ↓ ↓ ↓ │ │ Lightning Network │ └──────────────────────────────────────────────────────────────────────────┘ ``` **Wallet Priority:** When multiple wallets are configured, the MCP server selects them in order: **LND > NWC > Strike > OpenNode**. This order is optimized for L402 compatibility (LND and NWC always return preimages). Override with the `WALLET_PRIORITY` environment variable or the `priority` field in your config file. ## Installation ### .NET (Windows, Linux, macOS) ```bash dotnet tool install -g LightningEnable.Mcp ``` ### Python (All Platforms) ```bash pip install lightning-enable-mcp ``` Or use uvx for no-install execution: ```bash uvx lightning-enable-mcp ``` :::note NWC wallets The base Python install supports NWC wallets on every platform, including Windows, with no build toolchain. The old `pip install lightning-enable-mcp[nwc]` still works, but the `[nwc]` extra adds nothing. ::: ### Updating To update an existing install (`docs` needs 2.2.0 or later; `renew_lightning_enable_subscription` needs 2.1.0 or later): ```bash dotnet tool update -g LightningEnable.Mcp # .NET pip install --upgrade lightning-enable-mcp # Python uvx --refresh lightning-enable-mcp # Python with uvx ``` ## Quick Start **Claude Code** users: register the server with `claude mcp add --transport stdio lightning-enable --env STRIKE_API_KEY=your-key -- lightning-enable-mcp` (or a project `.mcp.json`) — see [Claude Code Setup](/products/agentic-commerce/claude-code-setup). Claude Code does **not** read `claude_desktop_config.json`. **Claude Desktop** users: add to your Claude Desktop config: **Windows:** `%APPDATA%\Claude\claude_desktop_config.json` **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json` **Linux:** `~/.config/claude/claude_desktop_config.json` ```json { "mcpServers": { "lightning-enable": { "command": "lightning-enable-mcp", "env": { "STRIKE_API_KEY": "your-strike-api-key" } } } } ``` --- ## All Available Tools ### Tool Profiles The MCP server ships three tool profiles, selected once at startup with `LIGHTNING_ENABLE_TOOL_PROFILE`: ```bash export LIGHTNING_ENABLE_TOOL_PROFILE=standard # default — omit this line to get the same result ``` | Profile | Surface | Use it when | |---|---|---| | `lite` | Pay, check the wallet, budget, receipts, wallet setup | You want the smallest possible footprint — an agent that can pay and check its own wallet, and nothing else. | | `standard` *(default)* | Every current tool (listed below) | The current, canonical surface. Everything the server can do, addressed through a small number of action-based tools instead of one tool per operation. | | `full` | `standard` plus every pre-consolidation name, listed as a **deprecated alias** | Your agent discovers tools from the server's listing and its prompts use the old per-operation names (`configure_budget`, `create_l402_challenge`, …). Aliases forward to the consolidated tools and are removed in **v3.0.0**, so migrate when you can. | Profiles change only what the server lists. Every tool and every deprecated alias stays callable by name under every profile, so a narrower profile never breaks a call that names a tool directly; it only keeps that tool's schema out of the agent's context. **`lite`:** `pay_invoice`, `access_l402_resource`, `get_balance`, `budget`, `receipts`, `setup_wallet` **`standard` (default):** `access_l402_resource`, `pay_invoice`, `pay_l402_challenge`, `test_l402_payment`, `get_balance`, `budget`, `receipts`, `create_invoice`, `check_invoice_status`, `verify_confirmation_code`, `discover_api`, `docs`, `create_lightning_enable_account`, `renew_lightning_enable_subscription`, `setup_wallet`, `wallet_ops`, `l402_producer`, `agent_services` `renew_lightning_enable_subscription` pays for Agentic Commerce in sats (30 days over L402, at the price `GET /api/merchant/subscription/renew` quotes) using your merchant API key and connected wallet; it requires MCP 2.1.0 or later. See [Renew in sats](/getting-started/activate-with-lightning#renew-in-sats). `docs` searches and reads this documentation site from inside the MCP server, so an agent without web access can look things up. It requires MCP 2.2.0 or later. See [docs](#docs). These lists mirror the server's own guard tests. The server's tool listing is authoritative for the version you have installed. ### Tool Availability by Wallet | Tool | Strike | OpenNode | LND | NWC (CoinOS/CLINK) | NWC (Alby) | NWC (Primal) | API key | |------|--------|----------|-----|-------------------|------------|--------------|---------| | `pay_invoice` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | No | | `access_l402_resource` | ✅ | ❌ | ✅ | ✅ | ✅ | ❌ | No | | `pay_l402_challenge` | ✅ | ❌ | ✅ | ✅ | ✅ | ❌ | No | | `test_l402_payment` | ✅ | ❌ | ✅ | ✅ | ✅ | ❌ | No | | `get_balance` | ✅ (multi-currency) | ✅ | ✅ | ✅ | ✅ | ✅ | No | | `budget` (`action=status\|tighten`) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | No | | `receipts` (`source=durable\|session`) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | No | | `create_invoice` | ✅ | ✅ | ✅ | ✅* | ✅* | ✅* | No | | `check_invoice_status` | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | No | | `verify_confirmation_code` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | No | | `discover_api` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | No | | `docs` (`action=search\|read`) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | No | | `create_lightning_enable_account` | ✅ | ❌ | ✅ | ✅ | ✅ | ❌ | No | | `renew_lightning_enable_subscription` | ✅ | ❌ | ✅ | ✅ | ✅ | ❌ | **Yes** | | `setup_wallet` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | No | | `wallet_ops` (`action=price\|exchange\|send_onchain`) | ✅ | ❌** | ✅ (send_onchain only) | ❌** | ❌** | ❌** | No | | `l402_producer` (`action=create\|verify\|configure_receive\|status\|create_proxy\|add_endpoint\|publish\|list_challenges`) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | **Yes, every action** | | `agent_services` (`action=discover\|request\|settle\|publish\|unpublish\|attest\|reputation`) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | Partial — see [ASA tools](#agent_services) | \*NWC invoice creation depends on wallet support. \*\*`price` and `exchange` are Strike-only regardless of wallet; `send_onchain` also works on LND. `docs` needs no wallet at all; it works before one is configured. L402 requires preimage return: LND always works, Strike works, CoinOS/CLINK work, Alby works, OpenNode/Primal don't return preimage. ### Deprecated aliases Two generations of renames, both kept working so an old prompt still makes sense — the mapping is what changed, not the behavior: | Deprecated name | Current call | Since | |---|---|---| | `check_wallet_balance`, `get_all_balances` | `get_balance` | v1.17.0 | | `confirm_payment` | `verify_confirmation_code` | v1.17.0 | | `get_payment_history` | `receipts(source="session")` | Tool consolidation | | `get_receipts` | `receipts(source="durable")` | Tool consolidation | | `get_budget_status` | `budget(action="status")` | Tool consolidation | | `configure_budget` | `budget(action="tighten")` | Tool consolidation | | `get_btc_price` | `wallet_ops(action="price")` | Tool consolidation | | `exchange_currency` | `wallet_ops(action="exchange")` | Tool consolidation | | `send_onchain` | `wallet_ops(action="send_onchain")` | Tool consolidation | | `create_l402_challenge` | `l402_producer(action="create")` | Tool consolidation | | `verify_l402_payment` | `l402_producer(action="verify")` | Tool consolidation | | `discover_agent_services` | `agent_services(action="discover")` | Tool consolidation | | `request_agent_service` | `agent_services(action="request")` | Tool consolidation | | `settle_agent_service` | `agent_services(action="settle")` | Tool consolidation | | `publish_agent_capability` | `agent_services(action="publish")` | Tool consolidation | | `unpublish_agent_capability` | `agent_services(action="unpublish")` | Tool consolidation | | `publish_agent_attestation` | `agent_services(action="attest")` | Tool consolidation | | `get_agent_reputation` | `agent_services(action="reputation")` | Tool consolidation | Every alias in the table dispatches under every profile. The profile decides only whether an alias is listed: the "Tool consolidation" aliases are listed under `LIGHTNING_ENABLE_TOOL_PROFILE=full`, and `standard` and `lite` list only the current names. The v1.17.0 aliases aren't listed under any profile. All the aliases are scheduled for removal in **v3.0.0**. --- ## Onboarding ### setup_wallet Reports which wallet the server pays from, or connects an NWC wallet. Call it with no arguments first: when no wallet is configured, it returns the setup steps. It is usually the first call on a fresh machine, and the one the [`producer-setup` skill](https://github.com/refined-element/lightning-enable-skills/tree/main/skills/producer-setup) opens with. **Parameters:** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `nwcConnectionString` | string | No | A `nostr+walletconnect://...` string (`nwc_connection_string` in Python). Omit to only report the current wallet. | With a connection string, the tool validates it, checks that it reaches a live wallet, and only then writes it to `wallets.nwcConnectionString` in `~/.lightning-enable/config.json`. If an environment variable already selects a wallet, it refuses and names the variable to unset, because environment variables override the config file. The response never contains the connection string. To use Strike, OpenNode, or LND, set their environment variables or config keys instead; see [Wallet Configuration](/products/agentic-commerce/mcp-wallet-setup). ## Core Tools (All Wallets) ### pay_invoice Pay any Lightning invoice and get the preimage as proof of payment. **Parameters:** | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `invoice` | string | Yes | - | BOLT11 Lightning invoice | | `max_sats` | int | No | 1000 | Maximum payment allowed (Python package; the .NET tool relies on budget-service limits) | | `confirmationNonce` | string | No | - | Confirmation code from the server console (`confirmation_nonce` in Python). Required on the retry when the first call returned `requiresConfirmation=true` — see [Out-of-Band Confirmation](#out-of-band-confirmation) | **Example:** ``` User: Pay this invoice: lnbc500n1p3... Claude: [Uses pay_invoice] Payment successful! - Amount: 500 sats - Preimage: 7f8a9b2c3d4e5f... - Provider: Strike ``` **Response:** ```json { "success": true, "provider": "Strike", "payment": { "preimage": "7f8a9b2c3d4e5f...", "amountSats": 500 }, "message": "Payment successful! Paid 500 sats." } ``` --- ### get_balance Get the connected wallet's balance: the sats balance plus, where available, all currency balances (multi-currency for Strike, a single BTC entry otherwise) and the session spend summary. Supersedes the old `check_wallet_balance` and `get_all_balances` tools (v1.17.0) — it returns the superset of what both returned, and the old names keep working as deprecated aliases until v3.0.0. **Parameters:** None required **Example:** ``` User: What's my wallet balance? Claude: [Uses get_balance] Wallet Balance: 50,000 sats (~$50 USD) Provider: Strike Strike balances: $127.45 USD · 0.00050000 BTC ``` **Response:** ```json { "success": true, "provider": "Strike", "balance": { "sats": 50000, "btc": 0.0005 }, "balances": [ { "currency": "USD", "available": 127.45, "formatted": "127.45 USD" }, { "currency": "BTC", "available": 0.0005, "formatted": "0.00050000 BTC (50,000 sats)" } ], "message": "Balance: 50,000 sats" } ``` --- ### receipts Read the payment record, from either of two sources selected with `source`. Replaces the two formerly separate tools `get_payment_history` (now `source="session"`) and `get_receipts` (now `source="durable"`). **Parameters:** | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `source` | string | Yes | - | `durable`: the append-only log at `~/.lightning-enable/receipts.jsonl` (survives restarts). `session`: payments made since this server process started (in memory). | | `limit` | int | No | - | Maximum records to return (`durable` clamps to 1-200) | **Example:** ``` User: Show my recent payments Claude: [Uses receipts with source="session"] Recent Payments: 1. 500 sats - lnbc500n1... - 5 min ago 2. 100 sats - lnbc100n1... - 1 hour ago 3. 1000 sats - lnbc1u1... - 2 hours ago Total: 1,600 sats across 3 payments ``` :::tip Also readable as an MCP Resource The durable log is also exposed as an MCP **Resource** at `lightning-enable://receipts`, so a client that reads resources rather than calling tools can pull the same data without a tool call. ::: --- ### budget Read or tighten spending limits, selected with `action`. Replaces the two formerly separate tools `get_budget_status` (now `action="status"`) and `configure_budget` (now `action="tighten"`). **Parameters:** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `action` | string | Yes | `status` (read-only) or `tighten` (lower the runtime caps) | | `perRequest` / `perSession` | int | No | `tighten` only: new per-payment and per-session caps in sats (`per_request` / `per_session` in Python). They default to 1,000 and 10,000, so pass both. | `tighten` can only lower a cap, never raise it above the config file. To raise a limit, edit `~/.lightning-enable/config.json` and restart the server. **Example — status:** ``` User: What are my spending limits? Claude: [Uses budget with action="status"] Budget Configuration (READ-ONLY): - Config file: ~/.lightning-enable/config.json - Auto-approve: up to $0.10 - Log & approve: $0.10 - $1.00 - Requires out-of-band confirmation code: above $1.00 - Maximum per payment: $500.00 - Maximum per session: $100.00 Session: - Spent: $0.45 (4,500 sats) - Remaining: $99.55 Note: AI cannot RAISE budget limits. budget(action="tighten") can only tighten the runtime sats caps; edit config.json to raise limits. ``` **Example — tighten:** ``` User: Lower my per-request cap to 5,000 sats for this session. Claude: [Uses budget with action="tighten", perRequest=5000, perSession=10000] Runtime per-payment cap tightened to 5,000 sats for this session. (This cannot be undone by calling budget again with a higher value — only editing config.json can raise it back.) ``` The operator's own sats limits are config-file keys; see [Sats-native budgets](#sats-native-budgets). --- ### verify_confirmation_code Verify whether a payment confirmation code (relayed by the human from the server console) is still valid and what it authorizes. **Verification only — it never executes a payment.** To actually pay, re-call the original payment tool with the `confirmationNonce` (.NET) / `confirmation_nonce` (Python) parameter. Renamed from `confirm_payment` in v1.17.0 (the old name still works as a deprecated alias until v3.0.0). **Parameters:** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `nonce` | string | Yes | The confirmation code the human read from the server console | **Response (valid):** ```json { "valid": true, "amount_sats": 2500, "tool": "pay_invoice", "message": "Code verified — NOTHING HAS BEEN PAID. To execute, call pay_invoice again with confirmation_nonce=." } ``` --- ## Invoice Tools (Strike, OpenNode, LND) ### create_invoice Create a Lightning invoice to receive payments. Returns a BOLT11 string to share with the payer. **Parameters:** | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `amountSats` | long | Yes | - | Amount to receive in satoshis | | `memo` | string | No | null | Description for the invoice | | `expirySecs` | int | No | 3600 | Invoice expiry (default 1 hour) | **Example:** ``` User: Create an invoice for 1000 sats for "Coffee payment" Claude: [Uses create_invoice] Invoice Created! - Amount: 1,000 sats - Memo: Coffee payment - Expires: 1 hour - Invoice ID: inv_abc123... BOLT11 Invoice (share with payer): lnbc10u1p3xyza... Share this invoice with the payer, or display as a QR code. ``` **Response:** ```json { "success": true, "provider": "Strike", "invoice": { "id": "inv_abc123", "bolt11": "lnbc10u1p3xyza...", "amountSats": 1000, "expiresAt": "2024-01-15T14:30:00Z" }, "message": "Invoice created for 1000 sats. Share the bolt11 string with the payer." } ``` --- ### check_invoice_status Check if a previously created invoice has been paid. **Parameters:** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `invoiceId` | string | Yes | Invoice ID from create_invoice | **Example:** ``` User: Has the coffee invoice been paid? Claude: [Uses check_invoice_status with invoiceId="inv_abc123"] Invoice Status: PAID! ✅ - Amount: 1,000 sats - Paid at: 2024-01-15 13:45:22 UTC ``` **Response:** ```json { "success": true, "provider": "Strike", "invoice": { "id": "inv_abc123", "state": "paid", "isPaid": true, "isPending": false, "amountSats": 1000, "paidAt": "2024-01-15T13:45:22Z" }, "message": "Invoice inv_abc123 has been PAID!" } ``` --- ## wallet_ops Strike- and LND-specific operations. One consolidated tool for the operations that only some wallets support, selected with `action`. Replaces the three formerly separate tools `get_btc_price` (now `action="price"`), `exchange_currency` (now `action="exchange"`), and `send_onchain` (now `action="send_onchain"`). `price` and `exchange` are Strike-only regardless of wallet. `send_onchain` works with Strike and LND. ### action="price" Get the current Bitcoin price in USD. **Parameters:** `action="price"` only — no other parameters **Example:** ``` User: What's the current Bitcoin price? Claude: [Uses wallet_ops with action="price"] Current BTC Price: $97,500.00 USD Source: Strike Timestamp: 2024-01-15 13:30:00 UTC ``` **Response:** ```json { "success": true, "provider": "Strike", "ticker": { "btcUsd": 97500.00, "timestamp": "2024-01-15T13:30:00Z" }, "message": "Current BTC price: $97,500.00 USD" } ``` --- ### action="exchange" Convert between USD and BTC within your Strike wallet. **Parameters:** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `sourceCurrency` | string | Yes | Currency to convert from: `USD` or `BTC` | | `targetCurrency` | string | Yes | Currency to convert to: `BTC` or `USD` | | `amount` | decimal | Yes | Amount in source currency | **Example — Buy Bitcoin:** ``` User: Convert $50 to Bitcoin Claude: [Uses wallet_ops with action="exchange", sourceCurrency=USD, targetCurrency=BTC, amount=50] Exchange Complete! - Converted: $50.00 USD - Received: 0.00051282 BTC (51,282 sats) - Rate: $97,500/BTC - Fee: $0.25 ``` **Example — Sell Bitcoin:** ``` User: Sell 0.001 BTC for dollars Claude: [Uses wallet_ops with action="exchange", sourceCurrency=BTC, targetCurrency=USD, amount=0.001] Exchange Complete! - Converted: 0.001 BTC (100,000 sats) - Received: $97.00 USD - Rate: $97,500/BTC - Fee: $0.50 ``` --- ### action="send_onchain" Send an on-chain Bitcoin payment to a Bitcoin address (not Lightning). Works with Strike and LND. **Parameters:** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `address` | string | Yes | Bitcoin address (bc1q..., 3..., or 1...) | | `amountSats` | long | Yes | Amount to send in satoshis | | `confirmationNonce` | string | No | Confirmation code from the server console (`confirmation_nonce` in Python). Omit on the first call to request one; **always required to actually send** | :::warning Always Confirmed On-chain payments are irreversible, so `wallet_ops(action="send_onchain")` **always** requires an out-of-band confirmation code — even for small amounts — and **fails closed** if the budget service is unavailable. See [Out-of-Band Confirmation](#out-of-band-confirmation). ::: **Example:** ``` User: Send 50000 sats to bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh Claude: [Uses wallet_ops with action="send_onchain"] On-Chain Payment Sent! - Amount: 50,000 sats (0.0005 BTC) - Address: bc1qxy2kg... - Network Fee: 500 sats - Status: COMPLETED - Transaction ID: abc123def456... ``` :::warning On-Chain vs Lightning On-chain payments are slower (10+ minutes) and have higher fees than Lightning. Use Lightning (`pay_invoice`) when possible. ::: --- ## L402 Tools These tools need no API key, but they need a wallet that returns the payment preimage: LND, Strike, or an NWC wallet such as CoinOS, CLINK, or Alby Hub. OpenNode and Primal don't return it, so they can't pay L402 challenges. ### access_l402_resource Fetch a URL with automatic L402 payment handling. When the API returns 402 Payment Required, the MCP automatically pays the invoice and retries with the L402 credential. **Parameters:** | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `url` | string | Yes | - | URL to fetch | | `method` | string | No | GET | HTTP method | | `headers` | string | No | null | JSON object of headers | | `body` | string | No | null | Request body | | `maxSats` | int | No | 1000 | Maximum payment allowed | | `confirmationNonce` | string | No | - | Confirmation code from the server console (`confirmation_nonce` in Python). Required on the retry when the first call returned `requiresConfirmation=true` | **Example:** ``` User: Fetch premium data from https://api.example.com/l402/data Claude: [Uses access_l402_resource] The API required a 50 sat payment which was automatically paid. Response: { "premium": "data...", "analysis": "..." } Payment Details: - Amount: 50 sats ($0.05) - L402 token cached for future requests ``` --- ### pay_l402_challenge Manually pay an L402 (or MPP) invoice when you have the invoice — and, for L402, the macaroon — as separate components. **Parameters:** | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `invoice` | string | Yes | - | BOLT11 Lightning invoice | | `macaroon` | string | No | - | Base64-encoded macaroon. **Optional — omit for MPP mode** | | `maxSats` | int | No | 1000 | Maximum payment allowed | | `confirmationNonce` | string | No | - | Confirmation code from the server console (`confirmation_nonce` in Python). Required on the retry when confirmation was requested | **Returns:** L402 credential in format `macaroon:preimage` (L402 mode), the bare preimage (legacy MPP mode), or a single-use `Authorization: Payment ` credential (modern MPP draft-00 mode) **MPP support:** the `macaroon` parameter is optional. When you omit it, the tool operates in MPP (Machine Payments Protocol) mode — some paid APIs issue a plain Lightning invoice and accept the payment preimage alone as the access token, with no macaroon involved. Provide the macaroon for standard L402 challenges; omit it when the challenge only gave you an invoice. **Modern `Payment` challenges (v1.24.0+):** for APIs using the current MPP draft-00 wire format (`draft-httpauth-payment-00`), pass the raw challenge header via the optional `challengeHeader` (.NET) / `challenge_header` (Python) parameter. The tool runs client-side safety checks before paying (expiry, `intent: charge`, sat currency, declared amount must agree with the invoice) and returns the single-use modern credential, which is never cached or replayed. `access_l402_resource` handles modern `Payment` challenges automatically and surfaces the server's `Payment-Receipt` header in its result (`paymentReceipt` — payment hash only, never the preimage). --- ## Reference Tools These tools look things up and never pay, so they don't need a wallet that returns a preimage. `docs` needs no wallet at all. ### discover_api Search the L402 API registry to find available paid APIs by keyword or category, or fetch a specific API's manifest for full endpoint details and pricing. **Parameters:** | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `query` | string | No | - | Search the registry by keyword (e.g., "weather", "ai") | | `category` | string | No | - | Filter registry results by category | | `url` | string | No | - | Fetch a specific API's manifest directly | | `budgetAware` | bool | No | true | Annotate results with affordable call counts | **Example — Search the registry:** ``` User: Find me some weather APIs I can pay for Claude: [Uses discover_api with query="weather"] Found 3 weather APIs in the L402 registry: 1. Weather Data API — 5 sats/request (1,600 calls affordable) 2. Storm Tracker Pro — 25 sats/request (320 calls affordable) 3. Climate Analytics — 100 sats/request (80 calls affordable) Want me to get full details on any of these? ``` **Example — Get full manifest:** ``` User: Show me the full details for Weather Data API Claude: [Uses discover_api with url="https://api.lightningenable.com/l402/proxy/weather-api/.well-known/l402-manifest.json"] Weather Data API — 3 endpoints: - GET /v1/current — Current weather (5 sats) - GET /v1/forecast — 7-day forecast (10 sats) - GET /v1/historical — Historical data (25 sats) ``` **Registry URL:** Defaults to `https://api.lightningenable.com`. Override with `L402_REGISTRY_URL` or `LIGHTNING_ENABLE_API_URL` environment variable. **Manifest probing (v1.24.1+):** when you pass a base `url`, the tool probes `/.well-known/l402-manifest.json`, `/.well-known/l402.json`, `/l402-manifest.json`, and `/l402.json` in order, and only accepts documents that actually describe a service or its endpoints (protocol signposts are skipped). --- ### docs Search or read the Lightning Enable documentation from inside the MCP server, so an agent can answer "how do I connect an NWC wallet?" or "what does `payment_provider_not_configured` mean?" without web access. Needs no wallet and no API key. Requires MCP 2.2.0 or later. The tool reads [`llms-full.txt`](https://docs.lightningenable.com/llms-full.txt), the whole docs site in one file, and keeps it in memory for an hour. If a refresh fails, it answers from the copy it already has and marks the result `stale: true` with a `warning`. If it has no copy yet, the error names the URL it tried, and the tool waits a minute before trying again. Redirects are not followed: a redirect is reported as an error, so point `LIGHTNING_ENABLE_DOCS_URL` at the final URL. **`action="search"`**: returns the best-matching sections for a free-text query. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `action` | string | Yes | - | `search` | | `query` | string | Yes | - | Free text, for example `nwc connection string` | | `limit` | int | No | 5 | Number of results, 1 to 20 | Each result has `title` (page title, and the heading when the match is a section), `path`, `url` (with the heading's anchor), `section` (the heading text, to pass to `read`), and `snippet`. Ranking counts how often the query's words appear in each section, and a word in the section's title counts five times as much as one in its body. **`action="read"`**: returns one page, or one section of it with its subsections, as text. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `action` | string | Yes | - | `read` | | `path` | string | Yes | - | A docs path such as `/nwc-setup/account-setup`. A full docs URL also works, and a `#anchor` on it selects a section | | `section` | string | No | whole page | Heading text, for example `Troubleshooting` | The result has `title`, `path`, `url`, `section`, `text`, `truncated`, and `sections` (the page's headings, so you can read a long page one section at a time). `text` is capped at 12,000 bytes; when it is cut, `truncated` is `true` and a `hint` points at `sections`. An unknown path returns an error with suggested paths; an unknown section returns the page's headings. **Example:** ``` User: My NWC invoices never confirm. What should I check? Claude: [Uses docs with action="search", query="nwc invoices never confirm"] Claude: [Uses docs with action="read", path="/nwc-setup/account-setup", section="Troubleshooting"] [Answers from the Troubleshooting section of the NWC setup guide, and links to it] ``` **Source URL:** defaults to `https://docs.lightningenable.com/llms-full.txt`. Override it with `LIGHTNING_ENABLE_DOCS_URL`: an `https://` URL, `http://` on localhost, or a local `file://` path (`file:///path` or `file://localhost/path`; network shares are refused). --- ## l402_producer The producer side of L402: everything your agent needs to **charge** other agents and users, configure where payments land, and stand up a monetized endpoint, selected with `action`. Every action requires `LIGHTNING_ENABLE_API_KEY`. A Free Producer Sandbox key works, within that plan's caps (3 endpoints, 200 challenges a month, 1,000 sats per challenge). No key yet? `create_lightning_enable_account` creates one. :::info Agents That Earn The consumer tools above let agents spend. `l402_producer` lets an agent **earn** — including setting up the whole receive path itself. Together, they enable true agent-to-agent commerce — AI agents that autonomously buy and sell services using Lightning payments. See [Sell With Your Agent](/getting-started/sell-with-your-agent) for the full end-to-end walkthrough that chains every action below. ::: | `action` | Replaces | Does | |---|---|---| | `create` | `create_l402_challenge` | Mint a Lightning invoice + macaroon to charge for a resource | | `verify` | `verify_l402_payment` | Verify an L402 token (macaroon + preimage) before granting access | | `configure_receive` | *(new)* | Point payouts at your own NWC wallet (pass `nwcConnectionString`, or omit it to reuse this server's NWC wallet) | | `status` | *(new)* | Read the plan, wallet, setup checklist, and recent mints (read-only) | | `create_proxy` | *(new)* | Wrap an existing API in an L402-gated proxy | | `add_endpoint` | *(new)* | Register a specific path, method, price, and description in the proxy's manifest | | `publish` | *(new)* | Enable the manifest and, optionally, list the proxy in the public L402 registry | | `list_challenges` | *(new)* | Read back the challenge feed — what's been minted and what's been paid | ### action="create" Create an L402 payment challenge (Lightning invoice + macaroon) to charge another agent or user. **Parameters:** | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `resource` | string | Yes | - | Resource identifier (URL, service name, or description) | | `priceSats` | long | Yes | - | Price in satoshis to charge | | `description` | string | No | null | Description shown on the Lightning invoice | **Example:** ``` User: Charge 50 sats for access to the forecast data Claude: [Uses l402_producer with action="create"] L402 challenge created! - Invoice: lnbc500n1p3xyza... - Price: 50 sats - Resource: /api/weather/forecast Share the invoice with the payer. After they pay, use l402_producer with action="verify" to confirm before granting access. ``` **Response:** ```json { "success": true, "challenge": { "invoice": "lnbc500n1p3xyza...", "macaroon": "AgELbGlnaHRuaW5n...", "paymentHash": "abc123def456...", "expiresAt": "2026-03-13T14:30:00Z" }, "resource": "/api/weather/forecast", "priceSats": 50, "message": "L402 challenge created for 50 sats. Share the invoice with the payer." } ``` --- ### action="verify" Verify an L402 token (macaroon + preimage) to confirm payment was made before granting access. **Parameters:** | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `macaroon` | string | Yes | - | Base64-encoded macaroon from the L402 token | | `preimage` | string | Yes | - | Hex-encoded preimage (proof of payment) | **Example:** ``` User: The agent sent this L402 token — verify it Claude: [Uses l402_producer with action="verify"] Payment verified! The payer paid 50 sats for /api/weather/forecast. Granting access now. ``` **Response (valid):** ```json { "success": true, "valid": true, "resource": "/api/weather/forecast", "message": "Payment verified. The payer has paid — you can now grant access to the resource." } ``` **Response (invalid):** ```json { "success": true, "valid": false, "message": "Payment verification failed. The token is invalid or the invoice has not been paid. Do NOT grant access." } ``` :::warning Always Verify Never grant access based on a payer claiming they paid. Always call `l402_producer` with `action="verify"` to cryptographically confirm payment before granting access. ::: ### action="configure_receive", "status", "create_proxy", "add_endpoint", "publish", "list_challenges" These six actions are new — there is no legacy single-purpose tool they rename. Together they let an agent take an API from "not connected to Lightning Enable at all" to "live, priced, and discoverable" without the dashboard: - **`configure_receive`** — wraps `PUT /api/merchant/nwc-connection`. - **`status`** — wraps `GET /api/merchant/quickstart`; confirm the receive lane actually took before anything downstream depends on it. - **`create_proxy`** — wraps `POST /api/proxy`. - **`add_endpoint`** — wraps `POST /api/proxy/{proxyId}/manifest/endpoints`. - **`publish`** — wraps `PUT /api/proxy/{proxyId}/manifest/settings`. - **`list_challenges`** — wraps `GET /api/l402/challenges`. Full parameters, request/response shapes, and a worked example chaining all eight actions (including `create`/`verify` above) are in [Sell With Your Agent](/getting-started/sell-with-your-agent); the underlying REST contract for each is in the [Producer API Reference](/products/agentic-commerce/producer-api-reference) and [Proxy Configuration](/products/agentic-commerce/proxy-configuration). See [L402 Producer API](/products/agentic-commerce/l402-producer-api) for the complete producer guide with end-to-end examples. --- ## agent_services Agent Service Agreements (ASA) over Nostr, live at `wss://agents.lightningenable.com` and [nostrwolfe.com](https://nostrwolfe.com). Agents discover each other's services, request one, settle it through an L402 payment, and review it (flow: discover → request → settle → attest). **`discover`, `settle`, and `reputation` work with just a wallet; `request`, `publish`, `unpublish`, and `attest` require `LIGHTNING_ENABLE_API_KEY`.** | `action` | Replaces | Purpose | API key | |------|---------|---------|---------| | `request` | `request_agent_service` | **Entry point.** Sends a service request (kind 38401 event) referencing the provider's capability. | Required | | `discover` | `discover_agent_services` | Discover agent capabilities on Nostr by category, hashtag, or keyword (kind 38400 events). | — | | `settle` | `settle_agent_service` | Settle an agreement via L402 payment (consumer/requester side) using the same auto-pay flow as `access_l402_resource`. | — | | `publish` | `publish_agent_capability` | Create an L402 proxy for your service and prepare its listing (kind 38400 event). The listing goes live only after you verify ownership of the target; until then the response has `published: false`. | Required | | `unpublish` | `unpublish_agent_capability` | Take a published listing down: retires the L402 proxy and publishes a NIP-09 deletion plus a status=removed replacement, so other agents stop seeing a dead listing. | Required | | `attest` | `publish_agent_attestation` | Publish a review (rating 1–5) for an agent after a completed agreement (kind 38403 event) to build its reputation. | Required | | `reputation` | `get_agent_reputation` | Fetch an agent's reputation score and reviews (queries kind 38403 attestations for a pubkey). | — | ### action="request" Sends a service request (kind 38401 event) referencing the provider's capability. The full ASA flow is **discover → request → settle → attest**. If the provider has an L402 endpoint, you can skip this step and use `action="settle"` directly. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `capabilityEventId` | string | Yes | - | Event ID of the capability to request | | `budgetSats` | int | Yes | - | Maximum budget in satoshis | | `parameters` | string | No | - | Additional parameters as JSON | ### action="discover" | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `category` | string | No | - | Filter by service category (e.g., `ai`, `data`, `translation`) | | `hashtags` | string[] | No | - | Filter by hashtags | | `query` | string | No | - | Search query | | `limit` | int | No | 20 | Maximum results to return | ### action="settle" Pays the L402 endpoint specified in the agreement, completing the service transaction. If you are the **provider** (selling a service), use `l402_producer` with `action="create"` to generate an invoice and `action="verify"` to confirm payment instead. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `l402Endpoint` | string | Yes | - | L402 endpoint URL from the service agreement | | `method` | string | No | `GET` | HTTP method (GET, POST) | | `body` | string | No | - | Optional request body for POST requests | | `agreementId` | string | No | - | Agreement event ID for tracking | | `maxSats` | int | No | 1000 | Maximum satoshis to pay | ### action="publish" Creates an L402 proxy for your target API and prepares its kind 38400 listing, but doesn't publish it yet. Other agents can't discover the service until you verify that you own the target. The response has `published: false`, `reason: "ownership_verification_required"`, a `verifyEndpoint`, and a one-time `verification.token` with instructions. Save the token: it isn't shown again. Place the token as the instructions describe, call `POST /api/proxy/{proxyId}/verify`, and then list the proxy with `PUT /api/proxy/{proxyId}/manifest/settings` and `"manifestPubliclyListed": true`. The proxy works by its direct L402 URL the whole time. See [Verify ownership to list publicly](./verify-ownership.md). | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `serviceId` | string | Yes | - | Unique service identifier (used as d-tag). An ID that starts with `shopify` is reserved for the Shopify integration and returns `400 reserved_proxy_id`. | | `categories` | string[] | Yes | - | Service categories (e.g., `['ai', 'translation']`) | | `content` | string | Yes | - | Description of the service | | `priceSats` | int | Yes | - | Price per request in satoshis | | `l402Endpoint` | string | No | - | L402 endpoint URL for payment settlement | | `targetUrl` | string | Yes | - | Target API URL. Publish always creates an L402 proxy for it, so a request without it returns `400`. | | `hashtags` | string[] | No | - | Hashtags for discoverability | ### action="unpublish" Works for marketplace listings created via the L402 proxy/dashboard pipeline. | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `serviceId` | string | Yes | - | The listing's identifier — its Nostr d-tag / proxy id | | `reason` | string | No | - | Optional free-text reason recorded on the removal event | ### action="attest" | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `subjectPubkey` | string | Yes | - | Pubkey of the agent being reviewed | | `agreementId` | string | Yes | - | Event ID of the agreement this review is for | | `rating` | int | Yes | - | Rating from 1–5 | | `content` | string | Yes | - | Free-text review content | ### action="reputation" | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `pubkey` | string | Yes | - | Pubkey of the agent to query reputation for | | `limit` | int | No | 20 | Maximum number of attestations to return | --- ## Wallet Configuration Set one wallet with environment variables in your MCP client config, or with the `wallets` block of `~/.lightning-enable/config.json` (environment variables win). Per-wallet steps are in [Wallet Configuration](/products/agentic-commerce/mcp-wallet-setup) and [LND Node Setup](/products/agentic-commerce/lnd-setup). | Wallet | Environment variables | Config keys | Returns preimage (L402) | |---|---|---|---| | LND | `LND_REST_HOST`, `LND_MACAROON_HEX` | `lndRestHost`, `lndMacaroonHex` | Yes | | NWC (CoinOS, CLINK, Alby Hub) | `NWC_CONNECTION_STRING` | `nwcConnectionString` | Yes | | NWC (Primal) | `NWC_CONNECTION_STRING` | `nwcConnectionString` | No | | Strike | `STRIKE_API_KEY` | `strikeApiKey` | Yes | | OpenNode | `OPENNODE_API_KEY`, `OPENNODE_ENVIRONMENT` | `openNodeApiKey`, `openNodeEnvironment` | No | When several are configured, the server picks **LND > NWC > Strike > OpenNode**. Override with `WALLET_PRIORITY` or `wallets.priority` (`lnd`, `nwc`, `strike`, or `opennode`). --- ## Budget Configuration Spending limits live in `~/.lightning-enable/config.json`, which the server creates on first run. In MCP 2.2.0+ the .NET and Python servers both write this file, and fall back to the same values for any key an existing file omits: ```json { "currency": "USD", "tiers": { "autoApprove": 0.10, "logAndApprove": 1.00, "formConfirm": 10.00, "urlConfirm": 100.00 }, "limits": { "maxPerPayment": 500.00, "maxPerSession": 100.00 }, "session": { "cooldownSeconds": 2, "requireApprovalForFirstPayment": true } } ``` Both `formConfirm` and `urlConfirm` need the same out-of-band confirmation code; if your client supports MCP elicitation, a payment above `formConfirm` also asks you to retype the amount. An explicit `null` on a limit means no limit. Tiers out of ascending order are clamped down to the tier above them, never raised. An omitted USD limit still applies when you set only sats limits, so write `"maxPerPayment": null, "maxPerSession": null` to rely on the sats limits alone. `session.cooldownSeconds` is the minimum gap between payments; `session.requireApprovalForFirstPayment` asks for a confirmation code on the first payment of every session. Add a `confirmation` block to choose where codes go (see [Out-of-Band Confirmation](#out-of-band-confirmation)). A hosted container (`LIGHTNING_ENABLE_HOSTED=1`) with no confirmation channel can't pay until you mount a config, turn off first-payment approval, or set a `webhook` or `file` channel; the server prints a startup warning in that state. The auto-pay paths (L402 auto-payment, `wallet_ops` `send_onchain`, `agent_services` `settle`) never prompt, so while first-payment approval is on they refuse a session's first payment on any channel; only `session.requireApprovalForFirstPayment: false` unblocks them. ### Approval Tiers With the defaults above: | Amount (USD) | Behavior | |--------------|----------| | First payment of a session (any amount) | Needs a confirmation code while `requireApprovalForFirstPayment` is `true` | | ≤ `autoApprove` ($0.10) | Paid without asking | | ≤ `logAndApprove` ($1.00) | Paid and logged | | Above `logAndApprove` | Needs an out-of-band confirmation code (see below) | | Above `maxPerPayment` ($500), or over `maxPerSession` ($100) in total | Refused | ### Sats-Native Budgets The USD values above are converted to sats at payment time from a live BTC price feed. If no price source answers, a USD check refuses the payment rather than guessing. To set limits directly in sats, add the sats keys under `limits`: ```json { "limits": { "maxPerPaymentSats": 50000, "maxPerSessionSats": 200000, "autoApproveSats": 1000 } } ``` You can set USD and sats limits together; the stricter one wins on every check. A sats limit needs no price feed, so it keeps working during a price-feed outage. The environment variables `LIGHTNING_ENABLE_MAX_PER_PAYMENT_SATS`, `LIGHTNING_ENABLE_MAX_PER_SESSION_SATS`, and `LIGHTNING_ENABLE_AUTO_APPROVE_SATS` set the same three values and override the file. ### Out-of-Band Confirmation When a payment needs confirmation, the server delivers a code over one of four channels, set with `confirmation.channel` in the config file or `LIGHTNING_ENABLE_CONFIRMATION_CHANNEL`: | `confirmation.channel` | Where the code goes | Use it when | |---|---|---| | `stderr` *(default)* | The server's console/stderr — the channel a human operator watching the terminal sees | Interactive local use — Claude Code, Claude Desktop, a terminal session | | `refuse` | Nowhere — the payment is refused outright rather than printed anywhere | Non-interactive / hosted contexts where nothing reads stderr, so printing a code there would be silently lost (or worse, readable by the wrong process) | | `webhook` | POSTed to `confirmation.webhookUrl` (signed with `confirmation.webhookSecret`) | A human is notified out-of-band (chat app, pager) rather than watching a terminal | | `file` | Written to `confirmation.filePath` | Headless or scheduled runs where no one is watching in real time | A code expires after `confirmation.ttlSeconds` (default 120 seconds, maximum 15 minutes); 300-600 suits the `webhook` and `file` channels. An unrecognized channel name refuses payments rather than falling back to `stderr`. `LIGHTNING_ENABLE_HOSTED=1` changes the *default*: when it is set and stdin is not a terminal, the channel defaults to `refuse` instead of `stderr`, because in a hosted, non-interactive deployment nobody is guaranteed to be watching stderr. Set the channel explicitly to `webhook` or `file` to deliver codes there instead. In every case the code is **never returned in a tool result**, so a prompt-injected agent can't read its own code and approve its own payment. The flow (channels other than `refuse`): 1. The agent calls the payment tool (`pay_invoice`, `access_l402_resource`, `pay_l402_challenge`, or `wallet_ops` with `action="send_onchain"`); the response says confirmation is required — without the code. 2. The server delivers the code over the configured channel, where you read it. 3. You give the code to the AI, which re-calls the **original** payment tool with the `confirmationNonce` (.NET) / `confirmation_nonce` (Python) parameter. The separate `verify_confirmation_code` tool only *verifies* a code — it never executes a payment. Codes are bound to the exact **amount, tool, and destination** (invoice, URL, or on-chain address), so a code can't be reused for a different payment. `wallet_ops` with `action="send_onchain"` **always** requires a code (on-chain sends are irreversible) and fails closed if the budget service is unavailable. No MCP tool can raise a limit: `budget(action="tighten")` only lowers the runtime caps, and `setup_wallet` writes only the wallet entry. These guarantees assume the agent can't read the server's stderr or edit files on its host; see [AI Spending Security](/products/agentic-commerce/ai-spending-security#threat-model) for that boundary. --- ## Environment Variables Reference | Variable | Required | Default | Description | |----------|----------|---------|-------------| | `STRIKE_API_KEY` | If using Strike | - | Strike API key | | `OPENNODE_API_KEY` | If using OpenNode | - | OpenNode API key | | `OPENNODE_ENVIRONMENT` | No | production | `production` or `dev` | | `NWC_CONNECTION_STRING` | If using NWC | - | Nostr Wallet Connect URI | | `LND_REST_HOST` | If using LND | - | LND REST API host | | `LND_MACAROON_HEX` | If using LND | - | LND admin macaroon in hex | | `LND_TLS_CERT_PATH` | No | - | Path to the node's `tls.cert` (PEM or DER). Needed for LND's self-signed certificate | | `LND_SKIP_TLS_VERIFY` | No | false | `true` skips LND TLS verification (development only) | | `LND_PAYMENT_TIMEOUT_SECONDS` | No | 25 | How long an LND payment may stay in flight before it is reported as pending | | `LND_FEE_LIMIT_SATS` | No | 5% of the invoice (minimum 2 sats) | Fixed routing-fee ceiling per LND payment | | `WALLET_PRIORITY` | No | lnd > nwc > strike > opennode | Force a specific wallet when several are configured (e.g., `nwc`) | | `LIGHTNING_ENABLE_API_KEY` | For `l402_producer` + some `agent_services` actions | - | Lightning Enable API key — required by every `l402_producer` action, and by the `agent_services` actions `request`/`publish`/`unpublish`/`attest` (`discover`/`settle`/`reputation` work without it). No key? `create_lightning_enable_account` provisions one for ~100 sats over Lightning | | `LIGHTNING_ENABLE_API_URL` | No | https://api.lightningenable.com | API base URL | | `L402_REGISTRY_URL` | No | falls back to `LIGHTNING_ENABLE_API_URL` | Registry endpoint used by `discover_api` | | `LIGHTNING_ENABLE_DOCS_URL` | No | `https://docs.lightningenable.com/llms-full.txt` | Source the `docs` tool reads: an `https://` URL, `http://` on localhost, or a `file://` path | | `LIGHTNING_ENABLE_TOOL_PROFILE` | No | `standard` | `lite`, `standard`, or `full` — see [Tool Profiles](#tool-profiles) | | `LIGHTNING_ENABLE_HOSTED` | No | unset | `1` in a non-interactive/hosted deployment: the confirmation channel defaults to `refuse`. See [Out-of-Band Confirmation](#out-of-band-confirmation) | | `LIGHTNING_ENABLE_CONFIRMATION_CHANNEL` | No | `stderr` | `stderr`, `refuse`, `webhook`, or `file`; overrides `confirmation.channel` | | `LIGHTNING_ENABLE_CONFIRMATION_WEBHOOK_URL` / `_WEBHOOK_SECRET` / `_FILE` / `_TTL_SECONDS` | No | - | Override the matching `confirmation.*` config keys | | `LIGHTNING_ENABLE_MAX_PER_PAYMENT_SATS` / `_MAX_PER_SESSION_SATS` / `_AUTO_APPROVE_SATS` | No | - | Sats limits; override the matching `limits.*Sats` config keys. See [Sats-native budgets](#sats-native-budgets) | The `L402_MAX_SATS_PER_REQUEST` and `L402_MAX_SATS_PER_SESSION` variables from older releases were removed and have no effect. The first time the server writes `~/.lightning-enable/config.json`, it restricts the file to your user (`chmod 0600` on macOS and Linux, `icacls` on Windows). If that fails, it prints a warning to stderr; restrict the file yourself. --- ## Security Best Practices 1. **Use a dedicated wallet** with only what you are willing to lose. 2. **Set budget limits** in `~/.lightning-enable/config.json` before first use. 3. **Review receipts** (`receipts` with `source="durable"`) after sessions. 4. **Never commit keys** or connection strings to version control. See [AI Spending Security](/products/agentic-commerce/ai-spending-security) for the threat model and incident response. --- ## Troubleshooting ### "Wallet not configured" Set one of: `STRIKE_API_KEY`, `NWC_CONNECTION_STRING`, `LND_REST_HOST`+`LND_MACAROON_HEX`, or `OPENNODE_API_KEY` ### "Budget limit exceeded" Payment exceeds your limits. Use `budget` with `action="status"` to see current limits, edit config file to adjust. ### "Tool not found" after upgrading An old name like `configure_budget` or `create_l402_challenge` still dispatches under every profile until v3.0.0, but `standard` and `lite` don't list it, so an agent that picks tools from the server's listing won't see it. Switch the prompt or integration to the consolidated tool (`budget(action="tighten")`, `l402_producer(action="create")`, …), or set `LIGHTNING_ENABLE_TOOL_PROFILE=full` to list the legacy names as deprecated aliases. From v3.0.0 the old names are removed and only the consolidated tools work. See [Tool Profiles](#tool-profiles) and [Deprecated aliases](#deprecated-aliases). ### "Feature not supported" Some features are wallet-specific (for example, BTC price is Strike-only). Check [Tool Availability by Wallet](#tool-availability-by-wallet). ### "L402 payment succeeded but access failed" Your wallet doesn't return preimage. Use **LND**, **CoinOS**, **CLINK**, **Alby Hub**, or **Strike**. OpenNode and Primal don't work for L402 (no preimage return). --- ## Try It: Lightning Enable Store The [Lightning Enable Store](https://store.lightningenable.com) is a live L402-powered web store where AI agents can purchase physical merchandise using Bitcoin Lightning payments. ``` Ask Claude: "Buy me a Lightning Enable t-shirt from store.lightningenable.com" ``` This demonstrates the full L402 flow: 1. **Browse catalog** — `GET /api/store/catalog` 2. **Checkout** — `POST /api/store/checkout` (returns HTTP 402 with invoice + macaroon) 3. **Pay invoice** — Use `pay_invoice` with the BOLT11 string 4. **Claim order** — `POST /api/store/claim` with L402 credential --- ## Next Steps - [L402 Producer API](/products/agentic-commerce/l402-producer-api) - **Make your agents earn** with L402 challenges - [MCP Wallet Setup](/products/agentic-commerce/mcp-wallet-setup) - Detailed wallet configuration - [AI Spending Security](/products/agentic-commerce/ai-spending-security) - Threat model and safeguards - [AI Spending Guidelines](/configuration/ai-spending-guidelines) - Suggested limits by use case ## Stuck? Start with [Troubleshooting](/troubleshooting) for the common failures and their fixes. If something's broken or not working, email support@lightningenable.com — a real person reads it. ============================================================================== # MCP Quick Start Source: https://docs.lightningenable.com/products/agentic-commerce/mcp-quickstart ============================================================================== # Lightning MCP: 3-Minute Setup Get your AI paying Lightning invoices in under 3 minutes. The MCP server is open-source (MIT) and free to install. Paying, invoicing, L402, budget, and API-discovery tools need only a wallet. Selling access over L402 and some agent-marketplace actions also need a Lightning Enable API key. The [MCP Complete Guide](/products/agentic-commerce/mcp-complete-guide) is the full reference. :::tip What You'll Get After this guide, Claude can: - Pay any Lightning invoice - Access L402-protected APIs with automatic micropayments - Check your wallet balance and payment history - Buy from L402 stores (like [agent-commerce.store](https://agent-commerce.store)) ::: --- ## Try L402 Now (No Setup Required) Before installing anything, you can verify L402 works with a single curl: ```bash curl https://api.lightningenable.com/l402/test/ping ``` You'll get a `402 Payment Required` response with a 1-sat Lightning invoice. Pay the invoice with any Lightning wallet, then retry with the L402 token: ```bash curl -H "Authorization: L402 :" \ https://api.lightningenable.com/l402/test/ping ``` You'll get a `200 OK` confirming L402 is working. No API key or signup required. --- ## What You Need 1. **Claude Code or Claude Desktop** — Already installed 2. **A Strike account** — Free, 2-minute signup --- ## Step 1: Get a Strike API Key (1 minute) [Strike](https://strike.me) is the fastest way to start. No infrastructure, no node, full L402 support. 1. Create an account in the Strike app or at [strike.me](https://strike.me). 2. Sign in at [dashboard.strike.me](https://dashboard.strike.me) and create an API key. 3. Copy the key. :::tip Why Strike? Strike requires zero infrastructure, supports L402 (returns preimage on every payment), and handles custody. Want alternatives? See [Wallet Options](/products/agentic-commerce/mcp-wallet-setup) for NWC wallets (CoinOS, Alby Hub, CLINK) or [LND](/products/agentic-commerce/lnd-setup) for self-hosted. ::: ### Fund Your Wallet Send a small amount of Bitcoin to your Strike wallet, or buy directly in the Strike app. Start with $1-5 worth. --- ## Step 2: Install the MCP Server (30 seconds) ```bash # .NET (recommended) dotnet tool install -g LightningEnable.Mcp # Or Python pip install lightning-enable-mcp ``` Already installed? Update to the latest release (2.1.0 or later is needed for `renew_lightning_enable_subscription`): ```bash dotnet tool update -g LightningEnable.Mcp # .NET pip install --upgrade lightning-enable-mcp # Python uvx --refresh lightning-enable-mcp # Python with uvx ``` :::info .NET not installed? Download the .NET SDK from https://dot.net/download (the tool runs on .NET 8, 9, or 10) ::: :::note NWC wallets The base Python install supports NWC wallets on every platform. The old `[nwc]` extra still installs but adds nothing. ::: --- ## Step 3: Configure Claude (1 minute) Claude Code and Claude Desktop are configured differently — pick the one you use. ### Claude Code (CLI) Register the server with the `claude mcp add` command: **.NET:** ```bash claude mcp add --transport stdio lightning-enable \ --env STRIKE_API_KEY=your-strike-api-key \ -- lightning-enable-mcp ``` **Python:** ```bash claude mcp add --transport stdio lightning-enable \ --env STRIKE_API_KEY=your-strike-api-key \ -- uvx lightning-enable-mcp ``` Alternatively, for a project-scoped setup, create a `.mcp.json` file in your project root (don't commit API keys): ```json { "mcpServers": { "lightning-enable": { "command": "lightning-enable-mcp", "env": { "STRIKE_API_KEY": "your-strike-api-key" } } } } ``` Restart Claude Code (or run `/mcp` to check the server is connected). ### Claude Desktop Edit the Claude Desktop config file: **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json` **Windows:** `%APPDATA%\Claude\claude_desktop_config.json` **Linux:** `~/.config/claude/claude_desktop_config.json` **.NET:** ```json { "mcpServers": { "lightning-enable": { "command": "lightning-enable-mcp", "env": { "STRIKE_API_KEY": "your-strike-api-key" } } } } ``` **Python:** ```json { "mcpServers": { "lightning-enable": { "command": "uvx", "args": ["lightning-enable-mcp"], "env": { "STRIKE_API_KEY": "your-strike-api-key" } } } } ``` Restart Claude Desktop after saving. ### Other Clients - [Cursor Setup](/products/agentic-commerce/cursor-setup) — `.cursor/mcp.json` - [Claude Code Setup](/products/agentic-commerce/claude-code-setup) — Full Claude Code walkthrough --- ## Step 4: Test It! Open Claude and try: ### Check Your Balance ``` Check my Lightning balance ``` ### Pay an Invoice ``` Pay this Lightning invoice: lnbc... ``` ### Access an L402 API ``` Get weather data for New York from agent-commerce.store ``` Claude will automatically handle the L402 payment and return the data. ### Buy from the Lightning Enable Store ``` Buy me a Lightning Enable t-shirt from store.lightningenable.com ``` Claude browses the catalog, checks out, pays the invoice, and gives you a claim link. --- ## Set Spending Limits The server creates `~/.lightning-enable/config.json` on first run. Edit its `tiers` and `limits` before the first real payment, for example: ```json { "currency": "USD", "tiers": { "autoApprove": 0.10, "logAndApprove": 1.00, "formConfirm": 10.00 }, "limits": { "maxPerPayment": 10.00, "maxPerSession": 20.00 } } ``` | Threshold | Behavior | |-----------|----------| | Up to $0.10 | Paid without asking | | Up to $1.00 | Paid and logged | | $1.00 - $10.00 | Needs confirmation: the server prints a code to its console, and you give the code to the AI | | Over $10 per payment, or $20 per session | Refused | No MCP tool can raise these limits. Restart the server after editing the file. See [AI Spending Security](/products/agentic-commerce/ai-spending-security) for the full model, including sats-denominated limits. A Lightning Enable Store order (25,000+ sats) is above these example limits; [AI Spending Guidelines](/configuration/ai-spending-guidelines#typical-costs) shows what a purchase needs. --- ## Troubleshooting ### "Wallet not configured" Your Strike API key isn't being read. Check: 1. Environment variable is set correctly in your MCP config 2. Restart Claude after saving config changes 3. Try running `lightning-enable-mcp` directly to see errors ### "dotnet: command not found" Install the .NET SDK (8, 9, or 10) from https://dot.net/download ### "uvx: command not found" Install uv: `pip install uv` or `curl -LsSf https://astral.sh/uv/install.sh | sh` --- ## Next Steps - [Wallet Options](/products/agentic-commerce/mcp-wallet-setup) — NWC, LND, and other wallets - [MCP Complete Guide](/products/agentic-commerce/mcp-complete-guide) — full tool reference - [Agent Commerce Store](https://agent-commerce.store) — dozens of L402 APIs to test with --- ## Quick Reference | Setting | Where | |---------|-------| | Strike API Key | [dashboard.strike.me](https://dashboard.strike.me) | | Claude Code Config | `claude mcp add ...` or project `.mcp.json` | | Claude Desktop Config | `claude_desktop_config.json` (per-OS paths above) | | Budget Config | `~/.lightning-enable/config.json` | | MCP Install | `dotnet tool install -g LightningEnable.Mcp` | **Questions?** [Open an issue on GitHub](https://github.com/refined-element/lightning-enable-mcp/issues). ## Stuck? Start with [Troubleshooting](/troubleshooting) for the common failures and their fixes. If something's broken or not working, email support@lightningenable.com — a real person reads it. ============================================================================== # Wallet Configuration Source: https://docs.lightningenable.com/products/agentic-commerce/mcp-wallet-setup ============================================================================== # MCP Wallet Setup This guide covers all wallet options for the Lightning Enable MCP server. **The critical factor for L402 is whether the wallet returns preimage** - without it, L402 authentication fails. ## L402 Compatibility Matrix | Wallet | Returns Preimage | L402 Works | Custody | Best For | |--------|-----------------|------------|---------|----------| | **LND (Your Node)** | ✅ Always | ✅ Yes | You control | Power users, guaranteed L402 | | **NWC (CoinOS)** | ✅ Yes | ✅ Yes | Custodial | Free, easy, L402 works | | **NWC (CLINK)** | ✅ Yes | ✅ Yes | Custodial | Nostr users, L402 works | | **Alby** | ✅ Yes | ✅ Yes | You hold the keys | NWC compatible | | **Strike** | ✅ Yes | ✅ Yes | Custodial | USD users, easy setup, L402 works | | **Primal** | ❌ No | ❌ No | Custodial | Direct payments only | | **OpenNode** | ❌ No | ❌ No | Custodial | Direct payments only | :::danger Why preimage matters L402 builds its credential from the preimage (proof of payment). With a wallet that doesn't return it, the payment succeeds but API access fails. ::: ## Recommended Setup for L402 The `"command": "lightning-enable-mcp"` configurations on this page run the .NET global tool. Install it first with `dotnet tool install -g LightningEnable.Mcp`. For the Python package, use `"command": "uvx", "args": ["lightning-enable-mcp"]` instead. :::tip Let the agent connect an NWC wallet With the server already running, ask the agent to call `setup_wallet`. With no arguments it reports the current wallet and the setup steps; with an NWC connection string it checks the wallet is reachable and saves it to `~/.lightning-enable/config.json`. ::: ### Option 1: LND (Best for Guaranteed L402) **Why LND?** - You run your own Lightning node - LND **always** returns preimage - L402 is guaranteed to work - No third-party dependency - You run the node — you hold the keys **Requirements:** - Running LND node (local or remote) - REST API enabled - Admin macaroon **Configuration:** ```json { "mcpServers": { "lightning-enable": { "command": "lightning-enable-mcp", "env": { "LND_REST_HOST": "localhost:8080", "LND_MACAROON_HEX": "your-admin-macaroon-in-hex" } } } } ``` **Getting Your Macaroon in Hex:** ```bash # Linux/Mac: xxd -ps -c 1000 ~/.lnd/data/chain/bitcoin/mainnet/admin.macaroon # Windows PowerShell: [System.BitConverter]::ToString([System.IO.File]::ReadAllBytes("$env:LOCALAPPDATA\Lnd\data\chain\bitcoin\mainnet\admin.macaroon")) -replace '-','' ``` **Or use config file** (`~/.lightning-enable/config.json`): ```json { "wallets": { "lndRestHost": "localhost:8080", "lndMacaroonHex": "0201036c6e6402f801...", "priority": "lnd" } } ``` LND uses a self-signed TLS certificate, so also set `LND_TLS_CERT_PATH` to the node's `tls.cert`. See [LND Node Setup](/products/agentic-commerce/lnd-setup) for TLS, remote access, and macaroon scope. ### Option 2: NWC with CoinOS (Free, Easy) **Why CoinOS?** - Completely free - Browser-based, no install - Returns preimage - L402 works - Easy to set up :::note Python install for NWC The base `pip install lightning-enable-mcp` supports NWC on every platform. The old `[nwc]` extra still installs but adds nothing. ::: **Setup:** 1. Sign up at https://coinos.io 2. Go to Settings → NWC 3. Create a new NWC connection 4. Enable **auto-pay** for the connection (important for L402!) 5. Copy the connection string ```json { "mcpServers": { "lightning-enable": { "command": "lightning-enable-mcp", "env": { "NWC_CONNECTION_STRING": "nostr+walletconnect://..." } } } } ``` :::tip Windows Users If the connection string contains special characters that don't work in environment variables, use the config file instead: ```json { "wallets": { "nwcConnectionString": "nostr+walletconnect://...", "priority": "nwc" } } ``` ::: ### Option 3: NWC with Alby Hub (You Hold the Keys) **Why Alby Hub?** - You hold the keys — you control the funds - Returns preimage - L402 works - Connect to your own node **Setup:** 1. Deploy Alby Hub: https://albyhub.com?ref=magma 2. Connect to your Lightning node 3. Create NWC connection 4. Use same configuration as CoinOS above --- ### Option 4: Strike (Easy Setup, L402 Works) **Why Strike?** - Easy API key setup - USD and BTC balance management - Returns preimage via `lightning.preImage` - **L402 works** - Built-in currency exchange **Setup:** 1. Create an account at https://strike.me 2. Get your API key from https://dashboard.strike.me 3. Fund your Strike account with BTC ```json { "mcpServers": { "lightning-enable": { "command": "lightning-enable-mcp", "env": { "STRIKE_API_KEY": "your-strike-api-key" } } } } ``` :::tip Strike BTC Balance Strike payments use your BTC balance by default. Make sure you have BTC funded (not just USD) for Lightning payments. ::: Strike is the only wallet that supports every `wallet_ops` action (`price`, `exchange`, `send_onchain`) and multi-currency balances in `get_balance`. --- ## Wallets That Do NOT Support L402 ### OpenNode OpenNode is a payment software provider but **does not return preimage**. **Use for:** Direct payments, creating invoices **Cannot use for:** L402 auto-pay ```json { "env": { "OPENNODE_API_KEY": "your-opennode-api-key", "OPENNODE_ENVIRONMENT": "production" } } ``` --- ## Wallet Priority If multiple wallets are configured, they're used in this order (optimized for L402): 1. **LND** (if `LND_REST_HOST` + `LND_MACAROON_HEX` are set) 2. **NWC** (if `NWC_CONNECTION_STRING` is set) 3. **Strike** (if `STRIKE_API_KEY` is set) 4. **OpenNode** (if `OPENNODE_API_KEY` is set) Override with `WALLET_PRIORITY` environment variable or config file: ```json { "wallets": { "priority": "nwc" } } ``` --- ## Using Config File for Credentials Environment variables can be tricky (especially on Windows with special characters). The config file at `~/.lightning-enable/config.json` is often easier: ```json { "currency": "USD", "tiers": { "autoApprove": 0.10, "logAndApprove": 1.00, "formConfirm": 10.00, "urlConfirm": 100.00 }, "limits": { "maxPerPayment": 500.00, "maxPerSession": 100.00 }, "session": { "requireApprovalForFirstPayment": true, "cooldownSeconds": 2 }, "wallets": { "lndRestHost": "localhost:8080", "lndMacaroonHex": "0201036c6e64...", "nwcConnectionString": "nostr+walletconnect://...", "strikeApiKey": "your-strike-key", "openNodeApiKey": "your-opennode-key", "priority": "lnd" } } ``` --- ## Tool Availability Paying, invoicing, L402, budget, and API-discovery tools need only a wallet. Selling access with `l402_producer`, and the `agent_services` actions `request`, `publish`, `unpublish`, and `attest`, also need `LIGHTNING_ENABLE_API_KEY`. The [MCP Complete Guide](/products/agentic-commerce/mcp-complete-guide#tool-availability-by-wallet) lists which tools each wallet supports. --- ## Troubleshooting ### "L402 payment succeeded but access failed" Your wallet doesn't return preimage. Solutions: 1. **Best:** Switch to LND (run your own node) 2. **Easy:** Switch to NWC with CoinOS or Alby 3. **Also works:** Strike (returns preimage) 4. OpenNode will NOT work for L402 ### "temporary_channel_failure" or routing errors Lightning network routing issue. Try: - Wait a few minutes and retry - Ensure your wallet has sufficient balance - Check if the destination node is online ### "Wallet not configured" No wallet credentials found. Set one of: - `LND_REST_HOST` + `LND_MACAROON_HEX` - `NWC_CONNECTION_STRING` - `STRIKE_API_KEY` - `OPENNODE_API_KEY` Or use the config file at `~/.lightning-enable/config.json` ### NWC "Payment cancelled by user" Enable auto-pay in your NWC wallet: - CoinOS: Settings → NWC → Edit connection → Enable auto-pay - Alby: Check connection permissions Also set in config: ```json { "session": { "requireApprovalForFirstPayment": false } } ``` --- ## Security Notes - **No MCP tool can raise your spending limits.** `setup_wallet` writes only the wallet entry of the config file. An agent with shell or filesystem access to the host is outside this guarantee; see [AI Spending Security](/products/agentic-commerce/ai-spending-security#threat-model). - **Treat every credential like a password**: API keys, NWC connection strings, and macaroons can all spend. --- ## Next Steps - [MCP Complete Guide](/products/agentic-commerce/mcp-complete-guide) - Full MCP tool reference - [AI Spending Security](/products/agentic-commerce/ai-spending-security) - Budget configuration - [How It Works](/products/agentic-commerce/how-it-works) - L402 protocol details ============================================================================== # Native Integration — ASP.NET Core Source: https://docs.lightningenable.com/products/agentic-commerce/native-integration-aspnet ============================================================================== # Native L402 Integration — ASP.NET Core This walkthrough takes you from a vanilla ASP.NET Core app to charging Lightning payments per request in under 10 minutes. You'll install [`L402Server.AspNetCore`](https://www.nuget.org/packages/L402Server.AspNetCore), add one line of middleware + an attribute, and your existing endpoints become paid endpoints. > If you haven't picked an integration mode yet, start with the [Native Integration overview](./native-integration) to understand when Native mode is the right fit. :::tip Runnable example A complete, working ASP.NET Core integration lives at [`l402-example-aspnet`](https://github.com/refined-element/l402-example-aspnet) — clone it, set your API key, and curl a live 402 → pay → 200 flow before wiring your own app. ::: ## Prerequisites - .NET 8.0 or higher - An ASP.NET Core app - A Lightning Enable merchant API key, from **Dashboard → Settings → API Credentials** (any plan, including the Free Producer Sandbox) - A payment provider (Strike, OpenNode, or an NWC wallet) configured under **Dashboard → Settings → Payment Provider** ## Install ```bash dotnet add package L402Server.AspNetCore ``` (`L402Server` — the underlying SDK — is pulled in transitively. Both are MIT-licensed.) ## 30-second example ```csharp using L402Server.AspNetCore; var builder = WebApplication.CreateBuilder(args); builder.Services.AddL402AspNetCore(opts => { opts.ApiKey = builder.Configuration["LightningEnable:ApiKey"]!; }); var app = builder.Build(); app.UseRouting(); app.UseL402(); app.MapControllers(); app.Run(); ``` Then mark any controller action with `[L402(PriceSats = N)]`: ```csharp [ApiController] [Route("api/premium")] public class PremiumController : ControllerBase { [HttpGet("weather")] [L402(PriceSats = 100)] public IActionResult Weather() => Ok(new { temp = 72 }); } ``` ### Where the API key comes from The package doesn't read configuration or environment variables itself: `ApiKey` is the value you assign in the `AddL402AspNetCore` callback. The example above reads `LightningEnable:ApiKey` from ASP.NET Core configuration, so the default configuration sources apply, and a later source overrides an earlier one: 1. `appsettings.json` 2. `appsettings.{Environment}.json` 3. User secrets (Development environment only) 4. Environment variables: set `LightningEnable__ApiKey` (a double underscore stands for the `:` separator) 5. Command-line arguments `LIGHTNING_ENABLE_API_KEY` isn't read unless your own code reads it, as the [SDK example](#using-the-sdk-directly-without-the-middleware) further down does. A blank key, or an unresolved placeholder such as `${LIGHTNING_ENABLE_API_KEY}`, fails at startup when `AddL402AspNetCore` runs. That's the whole integration. The middleware: 1. Reads `Authorization: L402 :` from each request 2. If the matched endpoint has `[L402]` and no valid credential → mints a fresh challenge via Lightning Enable's hosted producer API and returns `402 Payment Required` 3. If a valid credential is present → executes the action Endpoints without `[L402]` pass through ungated. ### Which endpoint a token unlocks From `L402Server.AspNetCore` 0.2.0 (which depends on `L402Server` 0.2.0), the middleware sends the request's resource with every token verification, and the hosted API enforces the macaroon's path caveat. A token bought for one of your endpoints returns `401` on a different endpoint. The middleware resolves the resource for verification the same way it does for minting (`Resource` on the `[L402]` attribute, then `ResourceSelector`, then `Request.Path`), so a token it issued for an endpoint verifies on that endpoint. Merchant binding and expiry are always enforced by the hosted API. `EnforceResourceOnVerify` is `true` by default. Set it to `false` only if you compare `VerificationResult.Resource` yourself. The middleware doesn't send the price with the verification. If you charge different prices on the same resource (for example, a `PriceSelector` that reads a query parameter), a token bought at the lower price also passes at the higher one. Put the price-deciding input into the resource with `ResourceSelector`, or compare `VerificationResult.AmountSats` in your action. :::warning 0.1.x releases verify the macaroon and preimage only `L402Server.AspNetCore` 0.1.x and `L402Server` 0.1.x call the verify endpoint with only the macaroon and preimage. They don't pass or compare the request path or amount, so a token bought for one of your endpoints also passes verification on a different endpoint gated under the same API key, within the token's validity window (60 minutes by default). Upgrade to 0.2.0 or later. If you stay on 0.1.x and gate multiple prices or resources under one API key, add a resource assertion in your action: ```csharp [HttpGet("weather"), L402(PriceSats = 100)] public IActionResult Weather() { var result = (VerificationResult)HttpContext.Items[L402HttpContextKeys.VerificationResult]!; if (result.Resource != HttpContext.Request.Path) { return Unauthorized(new { error = "Token was purchased for a different resource" }); } return Ok(new { temp = 72 }); } ``` A single flat price across your gated surface is unaffected in practice, because every token you issue is for the same class of access. ::: ## What the caller sees ### Without payment ```bash curl -i https://your-api.example/api/premium/weather ``` ```http HTTP/1.1 402 Payment Required Content-Type: application/json WWW-Authenticate: L402 macaroon="AgELbWFjYXJvb24...", invoice="lnbc1u1p3..." { "error": "Payment Required", "l402": { "macaroon": "AgELbWFjYXJvb24...", "invoice": "lnbc1u1p3...", "amount_sats": 100, "payment_hash": "abc123...", "expires_at": "2026-05-12T01:00:00Z", "resource": "/api/premium/weather" } } ``` ### With payment ```bash curl -i https://your-api.example/api/premium/weather \ -H 'Authorization: L402 AgELbWFjYXJvb24...:deadbeef...' ``` ```http HTTP/1.1 200 OK Content-Type: application/json { "temp": 72 } ``` ## Pricing patterns ### Per-route attributes (compile-time prices) ```csharp [HttpGet("forecast"), L402(PriceSats = 100)] public IActionResult Forecast() => Ok(...); [HttpGet("premium-llm"), L402(PriceSats = 500, Description = "GPT-4 backed")] public IActionResult PremiumLlm() => Ok(...); ``` ### Global flat price (gates everything mounted under the middleware) ```csharp app.UseL402(opts => opts.DefaultPriceSats = 100); ``` ### Function-form pricing (variable per request) `PriceSelector` is a `Func>?` — note the nullable `int?`: ```csharp app.UseL402(opts => { opts.PriceSelector = ctx => ValueTask.FromResult( ctx.Request.Query["model"] == "premium" ? 500 : 100); }); ``` Resolution order: 1. `PriceSelector` — consulted first if set. Returning a non-null value decides the price for that request. Returning `null` means "no opinion" and falls through to the next step — it does **not** ungate the request. 2. `[L402(PriceSats = N)]` attribute on the matched endpoint 3. `DefaultPriceSats` on options 4. All three unset/null → request passes through ungated This lets you use `PriceSelector` for just the requests it recognizes (say, a surge-priced model parameter) while attributes and `DefaultPriceSats` cover everything else. ## The `[L402]` attribute in full `L402Attribute` has three properties and can target methods **or classes**: | Property | Type | Notes | |---|---|---| | `PriceSats` | `int` (required) | Price in satoshis, ≥ 1. Compile-time constant — for variable pricing use `PriceSelector` on the options. | | `Description` | `string?` | Embedded in the Lightning invoice; shown in the payer's wallet UI. | | `Resource` | `string?` | Optional override for the resource bound into the macaroon's path caveat. Defaults to the request path when omitted. | ### Class-level gating Apply `[L402]` to a controller class to gate every action on it at one price: ```csharp [ApiController] [Route("api/premium")] [L402(PriceSats = 100)] // every action on this controller costs 100 sats public class PremiumController : ControllerBase { [HttpGet("weather")] public IActionResult Weather() => Ok(new { temp = 72 }); [HttpGet("forecast")] public IActionResult Forecast() => Ok(new { days = 7 }); } ``` A method-level `[L402]` on an action takes precedence over a class-level one for that action. ### Resource override ```csharp [HttpGet("weather")] [L402(PriceSats = 100, Resource = "/api/premium/weather-v1")] public IActionResult Weather() => Ok(new { temp = 72 }); ``` The resource bound into the challenge resolves in this order: 1. `Resource` on the matched `[L402]` attribute 2. `ResourceSelector` on the middleware options 3. `HttpContext.Request.Path` (the default) ## Configuration reference `L402AspNetCoreOptions`: | Option | Type | Default | Notes | |---|---|---|---| | `DefaultPriceSats` | `int?` | `null` | Flat price applied when no `[L402]` attribute is present | | `PriceSelector` | `Func>?` | `null` | Variable pricing per request. A non-null return wins; `null` falls through to the `[L402]` attribute, then `DefaultPriceSats` | | `ResourceSelector` | `Func?` | `HttpContext.Request.Path` | Bound as a macaroon caveat | | `DescriptionSelector` | `Func?` | `null` | Shown in the payer's Lightning wallet | | `IdempotencyKeySelector` | `Func?` | `null` | Sends `X-Idempotency-Key` for retry-safe challenge minting | | `EnforceResourceOnVerify` | `bool` | `true` | 0.2.0 and later. Sends the resolved resource with each verification so the hosted API enforces the macaroon's path caveat. Set `false` to opt out | | `OnInvalidToken` | `Func?` | sends `401` | Custom handler | ## Accessing the verified credential in your action After a successful verification the middleware sets `HttpContext.Items[L402HttpContextKeys.VerificationResult]`. In a controller, read it via the `HttpContext` property that `ControllerBase` exposes (MVC does not bind `HttpContext` as an action **parameter** — a `Weather(HttpContext ctx)` signature won't be populated): ```csharp [HttpGet("weather"), L402(PriceSats = 100)] public IActionResult Weather() { var result = (VerificationResult)HttpContext.Items[L402HttpContextKeys.VerificationResult]!; _logger.LogInformation( "Served {Resource} for {Sats} sats ({Hash})", result.Resource, result.AmountSats, result.PaymentHash); return Ok(new { temp = 72 }); } ``` (In minimal APIs, an `HttpContext` handler parameter *is* injected — the property route is only needed inside controllers.) Useful for usage logging, per-endpoint analytics, fraud detection. ## Minimal API support `[L402]` is a regular `Attribute` so it works on minimal-API metadata too: ```csharp app.MapGet("/api/premium/weather", () => new { temp = 72 }) .WithMetadata(new L402Attribute { PriceSats = 100 }); ``` ## Using the SDK directly (without the middleware) If you need to mint a challenge or verify a token outside the request pipeline — from a background service, a hosted worker, an HTTP handler in a non-ASP.NET context — use `L402Server` directly: ```csharp using L402Server; var client = new L402ServerClient(new L402ServerOptions { ApiKey = Environment.GetEnvironmentVariable("LIGHTNING_ENABLE_API_KEY")!, }); var challenge = await client.CreateChallengeAsync(new CreateChallengeRequest { Resource = "/api/x", PriceSats = 100, }); var verification = await client.VerifyTokenAsync(new VerifyTokenRequest { Macaroon = mac, Preimage = pre, Resource = "/api/x", // 0.2.0 and later: enforce the path caveat AmountSats = 100, // 0.2.0 and later: enforce the amount caveat }); ``` `Resource` and `AmountSats` are optional. Set them to the resource and price you are gating, and the hosted API returns `Valid = false` for a token minted for a different resource or amount. Leave them out and the call verifies the macaroon and preimage only. ## Pipeline order The middleware needs to be placed AFTER `UseRouting()` (so it can read `[L402]` attribute metadata from the matched endpoint) and BEFORE the endpoint executor (`MapControllers`, `UseEndpoints`, etc.): ```csharp app.UseRouting(); app.UseAuthentication(); // any other auth middleware app.UseAuthorization(); app.UseL402(); // ← here app.MapControllers(); ``` If you put `UseL402()` before `UseRouting()` the middleware will see no matched endpoint and won't find `[L402]` attributes. If you put it after `MapControllers()` it'll never run. ## Custom failure handling Default behavior: invalid L402 token → `401 Unauthorized`. Override with `OnInvalidToken` to send a fresh `402` instead: ```csharp app.UseL402(opts => { opts.OnInvalidToken = async (ctx, failure) => { ctx.Response.StatusCode = 402; await ctx.Response.WriteAsJsonAsync(new { error = "Token rejected — please pay again", details = failure.Error, }); }; }); ``` When `OnInvalidToken` is supplied, the middleware does NOT send the default 401 and does NOT continue to the next middleware — your callback is fully responsible for producing the response. Write a status code + body via `ctx.Response`, or redirect via `ctx.Response.Redirect(...)`. The pipeline short-circuits after your callback returns. (The callback signature is `Func` — there's no `next` delegate passed in.) ## Troubleshooting ### `502 Bad Gateway` on every request The middleware returns `502` whenever the producer API refuses to mint or can't be reached. Common causes: - The API key you assign to `opts.ApiKey` is invalid. In the example above that's `LightningEnable:ApiKey` in configuration, or the `LightningEnable__ApiKey` environment variable. See [Where the API key comes from](#where-the-api-key-comes-from) - No payment provider is configured on your account (`payment_provider_not_configured`) - A Free Producer Sandbox cap was reached (`plan_endpoint_limit`, `plan_volume_limit`, `plan_price_limit`) - An outbound firewall blocks `api.lightningenable.com` See the [Producer API error table](./producer-api-reference#error-responses) for every code. ### Every request returns 402 even after payment Behind a reverse proxy? Confirm the `Authorization` header is being forwarded to the ASP.NET Core app. In Azure App Service and most reverse-proxy setups this just works; some custom proxies strip the header. >>> ### `[L402]` attributes don't seem to work Verify `app.UseL402()` is placed AFTER `app.UseRouting()` and BEFORE `app.MapControllers()` / `app.UseEndpoints()`. The middleware reads attribute metadata from the matched endpoint, which only exists after routing has run. ## Source and license Both packages are MIT-licensed open source: - [`L402Server.AspNetCore` on GitHub](https://github.com/refined-element/le-server-l402-aspnetcore-dotnet) - [`L402Server` on GitHub](https://github.com/refined-element/le-server-l402-dotnet) ## Next steps - [Producer API reference](./producer-api-reference) — full HTTP surface - [Native Integration — Express](./native-integration-express) — Node + Express version of this walkthrough - [Proxy setup walkthrough](./proxy-setup-walkthrough) — if Proxy mode is a better fit ============================================================================== # Native Integration — Express (Node.js) Source: https://docs.lightningenable.com/products/agentic-commerce/native-integration-express ============================================================================== # Native L402 Integration — Express (Node.js) This walkthrough takes you from a vanilla Express app to charging Lightning payments per request in under 10 minutes. You'll install [`l402-express`](https://www.npmjs.com/package/l402-express), add one line of middleware, and your existing endpoints become paid endpoints. > If you haven't picked an integration mode yet, start with the [Native Integration overview](./native-integration) to understand when Native mode is the right fit. :::tip Runnable example A complete, working Express integration lives at [`l402-example-node`](https://github.com/refined-element/l402-example-node) — clone it, set your API key, and curl a live 402 → pay → 200 flow before wiring your own app. ::: ## Prerequisites - Node.js 18 or higher - An Express app (4.x or 5.x) - A Lightning Enable merchant API key, from **Dashboard → Settings → API Credentials** (any plan, including the Free Producer Sandbox) - A payment provider (Strike, OpenNode, or an NWC wallet) configured under **Dashboard → Settings → Payment Provider** ## Install ```bash npm install l402-express l402-server ``` `l402-server` is the underlying SDK that `l402-express` calls; both are MIT-licensed and the middleware re-exports the SDK's types so most consumers won't import `l402-server` directly. ## 30-second example ```js import express from "express"; import { l402 } from "l402-express"; const app = express(); // Anything mounted here costs 100 sats per request. app.use("/api/premium", l402({ apiKey: process.env.LIGHTNING_ENABLE_API_KEY, priceSats: 100, })); app.get("/api/premium/weather", (_req, res) => { res.json({ temp: 72 }); }); app.listen(3000); ``` That's the whole integration. The middleware: 1. Reads `Authorization: L402 :` from each request 2. If absent → mints a fresh challenge via Lightning Enable's hosted producer API and returns `402 Payment Required` with the invoice in the `WWW-Authenticate` header and a JSON body 3. If present → verifies the credential via Lightning Enable. On valid → call `next()`; on invalid → respond `401 Unauthorized` Endpoints NOT mounted under `l402(...)` pass through untouched. So you can mix paid and free routes freely. ### Which route a token unlocks From `l402-express` 0.2.0 (which depends on `l402-server` 0.2.0), the middleware sends the request's resource with every `verifyToken` call, and the hosted API enforces the macaroon's path caveat. A token bought for one of your routes returns `401` on a different route. By default the middleware verifies against the same value it binds at minting (the `resource` option when you set it, otherwise `req.path`), so a token it issued for a route verifies on that route. Merchant binding and expiry are always enforced by the hosted API. The `verifyResource` option controls this. Leave it unset for the default above, set it to `false` to skip enforcement and compare `res.locals.l402.resource` yourself, or set it to a value or function to verify against a different resource than the one bound at minting. Two Express details decide what "the same route" means: - **Mount prefixes.** Inside `app.use("/api/premium", l402(...))`, Express strips the mount prefix, so `req.path` is mount-relative (`/weather`, not `/api/premium/weather`). Two mounts that share a sub-path, such as `/api/cheap/weather` and `/api/premium/weather`, both bind `/weather`, and a token from one passes on the other. Bind the full path so minting and verification both use it: ```js app.use("/api/premium", l402({ apiKey: KEY, priceSats: 500, resource: (req) => req.baseUrl + req.path, // bind the full path })); ``` - **Price.** The middleware doesn't send the price with the verification. If a `priceSats` function charges different prices on the same path (for example, by query parameter), a token bought at the lower price also passes at the higher one. Put the price-deciding input into `resource`, or compare `res.locals.l402.amountSats` in your handler. :::warning 0.1.x releases verify the macaroon and preimage only `l402-express` 0.1.x and `l402-server` 0.1.x call the verify endpoint with only the macaroon and preimage. They don't pass or compare the request path or amount, so a token bought for one of your routes also passes verification on a different route gated under the same API key, within the token's validity window (60 minutes by default). Upgrade to 0.2.0 or later. If you stay on 0.1.x and gate multiple prices or resources (for example, `/api/cheap` at 10 sats and `/api/premium` at 500 sats), compare `res.locals.l402.resource` against the current request's path in your handler: ```js router.get("/forecast", l402({ apiKey: KEY, priceSats: 100 }), (req, res) => { if (res.locals.l402.resource !== req.path) { return res.status(401).json({ error: "Token was purchased for a different resource" }); } res.json({ forecast: "7-day data..." }); }); ``` With a mount prefix, bind the full path with `resource: (req) => req.baseUrl + req.path` as shown above, and compare `res.locals.l402.resource` against `req.baseUrl + req.path`. A single mount with a single flat price is unaffected in practice, because every token you issue is for the same class of access. ::: ## What the caller sees ### Without payment ```bash curl -i https://your-api.example/api/premium/weather ``` ```http HTTP/1.1 402 Payment Required Content-Type: application/json WWW-Authenticate: L402 macaroon="AgELbWFjYXJvb24...", invoice="lnbc1u1p3..." { "error": "Payment Required", "l402": { "macaroon": "AgELbWFjYXJvb24...", "invoice": "lnbc1u1p3...", "amount_sats": 100, "payment_hash": "abc123...", "expires_at": "2026-05-12T01:00:00Z", "resource": "/api/premium/weather" } } ``` ### With payment After paying the Lightning invoice and extracting the preimage: ```bash curl -i https://your-api.example/api/premium/weather \ -H 'Authorization: L402 AgELbWFjYXJvb24...:deadbeef...' ``` ```http HTTP/1.1 200 OK Content-Type: application/json { "temp": 72 } ``` ## Variable per-request pricing Pass a function instead of a static `priceSats` to derive the price from the request: ```js app.use("/api/llm", l402({ apiKey: process.env.LIGHTNING_ENABLE_API_KEY, priceSats: (req) => req.query.model === "premium" ? 500 : 100, })); ``` The function can return a `number` or a `Promise`. Use it for tiered pricing, user-based pricing, dynamic cost-of-goods scenarios, etc. `resource` and `description` accept the same function-or-static shape. ## Configuration reference | Option | Type | Default | Notes | |---|---|---|---| | `apiKey` | `string` | required (one of `apiKey` / `client`) | Merchant API key | | `client` | `L402Server` | — | Pre-constructed SDK client; use to share across mounts | | `priceSats` | `number \| (req) => number \| Promise` | **required** | Price in satoshis, ≥ 1 | | `resource` | `string \| (req) => string \| Promise` | `req.path` | Bound as a macaroon caveat | | `verifyResource` | `string \| (req) => string \| Promise \| false` | the `resource` value | 0.2.0 and later. Resource sent with each verification so the hosted API enforces the path caveat. `false` opts out | | `description` | `string \| (req) => string \| undefined` | none | Shown in the payer's Lightning wallet | | `idempotencyKey` | `(req) => string \| undefined` | none | Sends `X-Idempotency-Key` for retry-safe challenge issuance. Without it, each request mints a fresh challenge | | `baseUrl` | `string` | `https://api.lightningenable.com` | Override producer API URL (testing) | | `onInvalidToken` | `(req, res, failure, next) => void \| Promise` | sends `401` | Custom failure handler — useful for sending a fresh `402` instead | ## Accessing the verified credential in your handler After a successful verification the middleware sets `res.locals.l402` so downstream handlers can see what was paid for: ```js app.get("/api/premium/weather", (_req, res) => { const { resource, amountSats, paymentHash } = res.locals.l402; console.log(`Served ${resource} for ${amountSats} sats (${paymentHash})`); res.json({ temp: 72 }); }); ``` Useful for usage logging, per-endpoint analytics, fraud detection. ## Mounting patterns ### One price for everything ```js app.use(l402({ apiKey: KEY, priceSats: 50 })); // every endpoint below this costs 50 sats ``` ### Different prices for different sub-paths ```js const fullPath = (req) => req.baseUrl + req.path; app.use("/api/cheap", l402({ apiKey: KEY, priceSats: 10, resource: fullPath })); app.use("/api/premium", l402({ apiKey: KEY, priceSats: 500, resource: fullPath })); ``` Binding the full path keeps a token from `/api/cheap/...` off a matching route under `/api/premium/...`. See [Which route a token unlocks](#which-route-a-token-unlocks). ### Per-route in a Router ```js import express from "express"; import { l402 } from "l402-express"; const router = express.Router(); router.get("/forecast", l402({ apiKey: KEY, priceSats: 100 }), forecastHandler); router.get("/historical", l402({ apiKey: KEY, priceSats: 50 }), historyHandler); router.get("/free-info", freeInfoHandler); // not gated app.use("/api", router); ``` ## Sharing one SDK client across mounts Each `l402(...)` call constructs its own internal `L402Server` instance. For high-throughput apps you may prefer one shared instance with shared HTTP connection pooling: ```js import { L402Server } from "l402-server"; import { l402 } from "l402-express"; const client = new L402Server({ apiKey: process.env.LIGHTNING_ENABLE_API_KEY }); app.use("/api/cheap", l402({ client, priceSats: 10 })); app.use("/api/premium", l402({ client, priceSats: 500 })); ``` Mutually exclusive with `apiKey` — pass one or the other. ## Custom failure handling By default an invalid `L402` token returns `401 Unauthorized`. Some merchants prefer to issue a fresh `402` so the caller can retry without having to make a separate request first: ```js app.use(l402({ apiKey: KEY, priceSats: 100, onInvalidToken: async (_req, res, failure, _next) => { // Send a fresh 402 challenge instead of 401. // (You'd typically mint a new challenge via the SDK here. Sketch only — // see the runnable app at // https://github.com/refined-element/l402-example-node for a complete // integration.) res.status(402).json({ error: "Token rejected; please pay again", details: failure.error }); }, })); ``` When `onInvalidToken` is supplied, the middleware does NOT send the default 401 — you are responsible for either sending a response or calling `next()`. ## Troubleshooting ### "Bad Gateway" 502 on every request The middleware returns `502` whenever the producer API refuses to mint; the `message` field carries the reason. Common causes: - `LIGHTNING_ENABLE_API_KEY` is missing or invalid - No payment provider is configured on your account (`payment_provider_not_configured`) - A Free Producer Sandbox cap was reached (`plan_endpoint_limit`, `plan_volume_limit`, `plan_price_limit`) - An outbound firewall blocks `api.lightningenable.com` See the [Producer API error table](./producer-api-reference#error-responses) for every code. ### Every request returns 402 even after payment The `Authorization` header probably isn't reaching your Express app. If you're behind a reverse proxy (nginx, Cloudflare, load balancer), confirm the `Authorization` header is being forwarded. ## Source and license Both packages are MIT-licensed open source: - [`l402-express` on GitHub](https://github.com/refined-element/le-server-l402-express-node) - [`l402-server` on GitHub](https://github.com/refined-element/le-server-l402-node) Issues, pull requests, and protocol-level discussion at [lightninglabs/L402](https://github.com/lightninglabs/L402) all welcome. ## Next steps - [Producer API reference](./producer-api-reference) — full HTTP surface if you ever want to call the underlying API directly - [Native Integration — ASP.NET Core](./native-integration-aspnet) — same flow for .NET - [Proxy setup walkthrough](./proxy-setup-walkthrough) — if you decide Proxy mode is a better fit ============================================================================== # Native Integration Source: https://docs.lightningenable.com/products/agentic-commerce/native-integration ============================================================================== # Native L402 Integration Lightning Enable supports two integration shapes. **Native mode** is the one where your API stays exactly where it is — same domain, same auth, same observability — and Lightning Enable handles the payment protocol via a small middleware you drop into your existing app. ## When to choose Native mode over Proxy mode | | **Proxy mode** | **Native mode** | |---|---|---| | Setup time | ~5 minutes in the dashboard | One `install` + one line of middleware | | Code changes to your API | None | Add one middleware registration | | Traffic flows through Lightning Enable | Yes | **No** — your domain, your servers | | You can keep your existing auth | Difficult (Authorization header is consumed by L402) | Yes | | Custom observability / rate limiting | Limited | Full control | | Best for | Public APIs, experiments, no-existing-auth | Commercial APIs, anything with sensitive infrastructure | If your API has its own authentication, custom rate limiting, observability you don't want to lose, or is hosted somewhere you can't change DNS for — **Native mode is the right answer**. ## The two halves of the protocol The L402 protocol has a consumer side and a producer side. Lightning Enable publishes packages for both: | Side | Audience | Packages | |---|---|---| | **Consumer** | Agents / clients that *pay* for paid APIs | [`l402-requests`](https://www.npmjs.com/package/l402-requests) (Node), [`L402Requests`](https://www.nuget.org/packages/L402Requests) (.NET), [`l402-requests`](https://pypi.org/project/l402-requests/) (Python) | | **Producer** | API providers that *charge* for their endpoints | **The packages described on this page** | If you're calling paid APIs from an agent, you want the **consumer** packages. If you're building a paid API that you want agents to pay for, you want the **producer** packages — that's what the rest of this page is about. ## Two layers: SDK and middleware Each language ships two packages: a **server SDK** (raw HTTP client for our producer API) and a **framework middleware** (drop-in for your web framework, built on top of the SDK). | Stack | SDK | Framework middleware | |---|---|---| | Node + TypeScript | [`l402-server`](https://www.npmjs.com/package/l402-server) | [`l402-express`](https://www.npmjs.com/package/l402-express) | | .NET | [`L402Server`](https://www.nuget.org/packages/L402Server) | [`L402Server.AspNetCore`](https://www.nuget.org/packages/L402Server.AspNetCore) | ### What the SDK does The SDK wraps two Lightning Enable hosted endpoints: - `POST /api/l402/challenges` → `createChallenge()` → mints a Lightning invoice + macaroon - `POST /api/l402/challenges/verify` → `verifyToken()` → validates an incoming L402 credential That's it. ~200 lines of typed HTTP-client glue. The SDK knows nothing about HTTP frameworks, routes, or middleware pipelines. You give it inputs, it calls Lightning Enable, you get outputs. Use the SDK directly if you're calling these from a background job, queue worker, serverless function, or anywhere that isn't a typical HTTP middleware. ### What the middleware does The framework middleware sits on top of the SDK and handles all the wiring for the 90% case (an HTTP API that wants to charge per request): 1. Read the `Authorization: L402 :` header 2. If absent → call `createChallenge()`, respond `402 Payment Required` with the invoice 3. If present → call `verifyToken()`. Valid → call next handler; invalid → respond `401` That's all it does — and it does it so you don't have to write the parse-call-respond dance in every paid route. Most merchants will only ever touch the middleware. The SDK is what the middleware imports under the hood. ### Resource and amount enforcement by version **0.2.0 and later** (`l402-server` / `l402-express` 0.2.0, `L402Server` / `L402Server.AspNetCore` 0.2.0): - The SDKs accept optional `resource` and `amountSats` on `verifyToken()` (`Resource` and `AmountSats` on `VerifyTokenRequest` in .NET). When you set them, the hosted API enforces the macaroon's path and amount caveats and reports a token minted for a different resource or price as invalid. - The middleware sends the request's resource with every verification by default, so a token bought for one of your paths returns `401` on a different path. Opt out with `verifyResource: false` (Express) or `EnforceResourceOnVerify = false` (ASP.NET Core). - The middleware doesn't send the price. If you charge different prices on the same resource, put the price-deciding input into the resource, or compare the verified amount in your handler. Each stack page shows how. **0.1.x** releases call the verify endpoint with only the macaroon and preimage. They don't pass `resource` or `amountSats`, and they don't compare the verified token's resource against the incoming request path. Within the token validity window (60 minutes by default), a token bought for one of your paths can pass verification on a different path of yours. Upgrade to 0.2.0 or later; if you can't, compare the verified resource in your handler as shown on the [Express page](./native-integration-express) and the [ASP.NET Core page](./native-integration-aspnet). On every version, the hosted API enforces merchant binding and expiry. ## Quick start by stack - **[Express (Node.js)](./native-integration-express)** — `npm install l402-express`, then one line of `app.use(l402({ apiKey, priceSats }))` - **[ASP.NET Core (.NET)](./native-integration-aspnet)** — `dotnet add package L402Server.AspNetCore`, then `app.UseL402()` + `[L402(PriceSats = 100)]` attribute - **Any other language** — call the producer API directly; see the [Producer API Reference](./producer-api-reference). ## Architectural decisions baked into the SDKs These are intentional. Worth knowing what you're getting: - **No protocol code on the client side.** Macaroon signing, preimage hashing, payment-hash linking — all server-side at Lightning Enable. The SDK is HTTP-client glue. If the protocol evolves, the hosted endpoint changes; you don't update your code. - **Verification via the hosted endpoint, not local key material.** Every `verifyToken` call goes to `/api/l402/challenges/verify`. One round-trip per paid request. We don't distribute the L402 root key to merchants — that's centralized for security and for replay protection. - **Token reuse within the validity window is a feature.** A paid token verifies again and again until its `expires` caveat (60 minutes by default), so an agent can make many quick calls on one payment. Merchant binding and expiry are always enforced; path and amount are enforced when the verify call includes `resource` and `amountSats`. For strict single-use semantics, track `paymentHash` in your handler. See [Token reuse within the validity window](./producer-api-reference#token-reuse-within-the-validity-window). - **No credentials stored anywhere.** Lightning Enable never asks for your upstream API credentials. The middleware never touches your authentication. Your secrets stay on your servers. ## Prerequisites - A Lightning Enable account. The free **Free Producer Sandbox** (3 endpoints, 200 challenges a month, 1,000 sats max per challenge) is enough to build and test; **Agentic Commerce** ($49/month, 30-day free trial) removes the caps. **Agentic Commerce — Business** is [contact us](mailto:support@lightningenable.com). - A payment provider — Strike (recommended), OpenNode, or your own wallet over Nostr Wallet Connect — saved under **Dashboard → Settings → Payment Provider**. - A Lightning Enable merchant API key, from **Dashboard → Settings → API Credentials**. - An existing HTTP API (or a fresh one) on Node + Express or .NET + ASP.NET Core. For another language, use the [Producer API Reference](./producer-api-reference). ## What the merchant pays for The middleware is open-source MIT. The SDK is open-source MIT. Lightning Enable's value is the hosted protocol broker — the API endpoint that mints macaroons, signs them with our root key, verifies preimages, enforces caveats, and talks to your payment provider (Strike, OpenNode, or your NWC wallet). Your plan pays for that broker, the dashboard, and the registry that lets agents discover your API. ## Testing your integration Three practical ways to exercise a Native integration before real traffic hits it: 1. **Pay real 1-sat invoices.** Set a **1-sat price** on a staging route and pay the invoices from your own wallet; you're out ~1 sat per test plus routing fees. (Hosted Lightning Enable runs OpenNode in production mode, so an OpenNode dev/testnet key won't mint here.) 2. **Unit-test without any network calls.** Both SDKs let you inject the HTTP layer, so you can stub the producer API's responses: - **Node:** the `L402Server` constructor accepts a custom `fetch` implementation (`new L402Server({ apiKey, fetch: mockFetch })`) — return canned challenge/verification JSON from your mock. - **.NET:** `L402ServerClient` has a constructor overload that accepts an external `HttpClient` (designed for `IHttpClientFactory`) — hand it an `HttpClient` built on a fake `HttpMessageHandler` that returns canned responses. 3. **Smoke-test against the live example apps.** The runnable reference apps at [`l402-example-node`](https://github.com/refined-element/l402-example-node) and [`l402-example-aspnet`](https://github.com/refined-element/l402-example-aspnet) are complete, working integrations — curl them to see exactly what a healthy 402 → pay → 200 flow looks like, and diff their setup against yours when something misbehaves. ## Next steps - [Pick your stack](#quick-start-by-stack) and follow the integration walkthrough - [Producer API Reference](./producer-api-reference) — full HTTP endpoint reference if you want to call our API directly - [Proxy setup walkthrough](./proxy-setup-walkthrough) — if Proxy mode is the right fit after all ============================================================================== # Overview Source: https://docs.lightningenable.com/products/agentic-commerce/overview ============================================================================== # Agentic Commerce :::tip Producer Account Required (free options available) **Creating** L402-protected API endpoints requires a Lightning Enable producer account. Start free with the **Free Producer Sandbox** (3 endpoints, 200 challenges/month, 1,000 sats max per challenge, 1 proxy — no card), or [activate a 30-day Agentic Commerce trial by paying a ~100-sat L402 challenge](/getting-started/activate-with-lightning) — no card. Full production use is **Agentic Commerce** ($49/month) or **Business** ([contact us](mailto:support@lightningenable.com)). **Using** the MCP server to access L402 APIs is completely free — no account needed. [View pricing](/products/product-overview) ::: L402 enables **pay-per-request API monetization** using the Lightning Network. Your callers pay a Lightning invoice to access your API: no accounts, no subscriptions, no credit cards. :::tip Pick a payment provider **Strike (recommended):** create a [Strike account](https://dashboard.strike.me) and generate an API key. Strike returns preimages, which L402 needs. **OpenNode:** use a production key (KYB takes 2-4 business days). **Nostr Wallet Connect:** receive straight into your own Lightning wallet. [Strike Setup →](/strike-setup/account-setup) | [OpenNode Setup →](/opennode-setup/account-setup) | [NWC Setup →](/nwc-setup/account-setup) ::: ## Zero Infrastructure Required Unlike other L402 implementations, Lightning Enable is a **fully hosted SaaS**: - **No Lightning node** — your payment provider (Strike or OpenNode) handles payment routing and liquidity - **No liquidity management** — no channel balancing, no inbound capacity planning - **No custom servers to run** — no Docker, no Kubernetes; we host the L402 protocol layer for you - **No protocol expertise** — configure via dashboard in minutes **What you need:** 1. A Lightning Enable producer account — the free [Producer Sandbox or no-card L402 Fast Lane trial](/getting-started/activate-with-lightning), or an Agentic Commerce ($49/month) / Business ([contact us](mailto:support@lightningenable.com)) subscription 2. A payment provider — [Strike](https://dashboard.strike.me) (recommended), OpenNode, or an NWC wallet 3. 5 minutes to configure your first proxy in the [dashboard](/products/agentic-commerce/dashboard-guide) ## What is L402? L402 (formerly LSAT) combines: - **HTTP 402 Payment Required** - The web's native payment status code - **Lightning Network** - Instant Bitcoin micropayments - **Macaroons** - Cryptographic bearer tokens with permissions ### The Problem Traditional API monetization requires: - User accounts and passwords - Credit cards on file - Monthly subscriptions (even for light usage) - Complex billing infrastructure - Fraud prevention systems ### The Solution With L402: - **No accounts** - Pay and access immediately - **No credit cards** - Bitcoin only - **Pay-per-use** - Only pay for what you consume - **Instant access** - Payment confirms in ~1 second - **Global** - Anyone with Bitcoin can access ## How It Works ``` 1. Client requests API 2. Server returns 402 ─────────────────────────────> <───────────────────── GET /api/premium/data HTTP 402 Payment Required Invoice: lnbc100n1p... Macaroon: AgEL... 3. Client pays invoice 4. Client retries with proof ─────────────────────> ─────────────────────────> Via Lightning wallet Authorization: L402 mac:preimage (~1 second) 5. Server verifies, grants access <───────────────────────── HTTP 200 OK { "data": "..." } ``` ## Key Benefits | Traditional API | L402 API | |-----------------|----------| | Account required | No account needed | | Credit card on file | No payment method stored | | Monthly subscription | Pay per request | | Minimum $5–$20 charge | As low as 1 sat (~$0.0004) | | Chargeback risk | Impossible to reverse | | KYC for users | Permissionless access | ## Agent-to-Agent Commerce (Producer API) :::tip New: Agents That Earn With the L402 Producer API, your AI agents can **charge** for their services — not just pay for others'. Your agent creates L402 payment challenges, other agents pay them, and your agent verifies payment before granting access. This is the missing piece for true agentic commerce. **One producer MCP tool, `l402_producer`, action-based:** - `action="create"` — Create a 402 challenge with Lightning invoice + macaroon - `action="verify"` — Verify an L402 token to confirm payment before granting access - Plus `action="configure_receive"`, `"status"`, `"create_proxy"`, `"add_endpoint"`, `"publish"`, and `"list_challenges"` — enough to take an API from unconfigured to live and discoverable without the dashboard. See [Sell With Your Agent](/getting-started/sell-with-your-agent) for the walkthrough. Beyond this, the MCP server also ships the agent-to-agent marketplace (`agent_services`) tool, `create_lightning_enable_account` (self-serve signup over Lightning), and `renew_lightning_enable_subscription` (pay for Agentic Commerce in sats, MCP 2.1.0 or later) — see the [MCP Complete Guide](/products/agentic-commerce/mcp-complete-guide) for the full tool list. [Read the full L402 Producer API guide](/products/agentic-commerce/l402-producer-api) ::: ## Use Cases - **API monetization** — charge per call, with a different price per endpoint (for example 100 sats for a transcription, 1,000 for an image). - **Premium content** — unlock articles, reports, or media per request. - **Data APIs** — pay-per-query market or analytics data. - **Proxy monetization** — put a paywall in front of any HTTP API, with no code changes. See [Proxy Configuration](/products/agentic-commerce/proxy-configuration). ## Quick Example ### Request Without Payment ```bash curl https://api.yourservice.com/api/premium/data ``` **Response: 402 Payment Required** ```json { "error": "Payment Required", "l402": { "macaroon": "AgELbGlnaHRuaW5nLWVuYWJsZQ...", "invoice": "lnbc100n1p3...", "amount_sats": 100, "payment_hash": "abc123..." } } ``` ### Pay the Invoice Use any Lightning wallet (Phoenix, Muun, Zeus, etc.) to pay. Get the preimage. ### Request With Payment Proof ```bash curl https://api.yourservice.com/api/premium/data \ -H "Authorization: L402 AgELbGlnaHRuaW5nLWVuYWJsZQ...:abc123def456..." ``` **Response: 200 OK** ```json { "data": "Premium content here..." } ``` ## Pricing **Agentic Commerce: $49/month** | **Agentic Commerce — Business: [Contact us](mailto:support@lightningenable.com)** Agentic Commerce includes: - Unlimited L402 endpoints - Strike as settlement provider - Per-endpoint pricing - Live dashboard + per-request payment feed Agentic Commerce — Business includes everything in Agentic Commerce, plus: - White-glove onboarding - Direct founder access (no ticket queues) Not sure where to start? The **Free Producer Sandbox** (3 endpoints, 200 challenges/month, 1,000 sats max per challenge, no card) is the risk-free way to prove L402 works before you subscribe. Lightning Enable charges no per-transaction fees. Your payment provider may charge its own processing fees; check its fee schedule. ## Architecture Lightning Enable handles the complexity: ``` +-----------------+ | Your API | | (Protected) | +--------+--------+ | v +--------+--------+ +------------------+ +-----------------+ | L402 Middleware |----->| Lightning Enable |----->| Strike/OpenNode | | (Auth Check) | | (Invoice/Verify) | | (Payment) | +-----------------+ +------------------+ +-----------------+ | | If valid L402 credential v +-----------------+ | Your API Logic | | (Execute) | +-----------------+ ``` :::info Hosted Service Lightning Enable runs as a hosted service at api.lightningenable.com. You configure it via the [dashboard](/products/agentic-commerce/dashboard-guide) or REST API — you don't deploy or manage it. ::: ## Getting Started 1. **Get a producer account** — start free with the [Producer Sandbox or the ~100-sat L402 Fast Lane trial](/getting-started/activate-with-lightning), or subscribe to Agentic Commerce ($49/month) / Business ([contact us](mailto:support@lightningenable.com)) 2. **Set up a payment provider:** - **Strike (recommended):** [Create account](https://dashboard.strike.me) → generate API key → ready immediately - **OpenNode:** complete KYB (2-4 business days) and generate a production key - **NWC:** create a connection in your wallet app 3. **Log in to the [dashboard](https://api.lightningenable.com/dashboard)** — enter your email for a magic link, open it, then type the same email to confirm 4. **Add your provider credential** in Dashboard → Settings → Payment Provider 5. **Create your first proxy** — point it at any API you want to monetize, or add the [native middleware](/products/agentic-commerce/native-integration) to your app 6. **Test a payment** — verify your integration end to end ## Next Steps - [Sell With Your Agent](/getting-started/sell-with-your-agent) - **Zero to a paid endpoint**, driven end to end by an MCP agent — no dashboard required - [L402 Producer API](/products/agentic-commerce/l402-producer-api) - **Make your agents earn** with L402 challenges - [Dashboard Guide](/products/agentic-commerce/dashboard-guide) - **Visual walkthrough with screenshots** - [How It Works](/products/agentic-commerce/how-it-works) - Technical deep dive - [MCP Complete Guide](/products/agentic-commerce/mcp-complete-guide) - **Full MCP documentation** - [API Monetization](/products/agentic-commerce/api-monetization) - Protect your APIs - [Proxy Configuration](/products/agentic-commerce/proxy-configuration) - Monetize any API - [OpenAPI document](/products/agentic-commerce/proxy-setup-walkthrough#openapi-document) - Every published proxy also serves a standard OpenAPI 3.1 document with `x-payment` pricing, for tools that don't speak the L402 manifest format - [AI Agent Integration](/products/agentic-commerce/ai-agent-integration) - Advanced MCP usage - [Wallet Configuration](/products/agentic-commerce/mcp-wallet-setup) - Detailed wallet setup ## Resources - [L402 Protocol Spec (Lightning Labs)](https://github.com/lightninglabs/L402) - [Lightning Network](https://lightning.network) - [Macaroons Paper](https://research.google/pubs/pub41892/) ============================================================================== # Producer API Reference Source: https://docs.lightningenable.com/products/agentic-commerce/producer-api-reference ============================================================================== # L402 Producer API Reference The producer API is what you call to sell: mint a challenge, verify the credential the payer brings back, read what you have minted, and get told when a challenge is paid. Everything an integrator needs is on this page. If you're on Node or .NET, the [SDKs and middleware](./native-integration) wrap these endpoints with idiomatic ergonomics — you can use them instead. This page is for direct integrators on stacks we don't ship a package for yet, or for understanding what the SDK is doing under the hood. | Endpoint | What it does | |---|---| | `POST /api/l402/challenges` | Mint an invoice + macaroon for a resource | | `GET /api/l402/challenges` | List what you have minted, with payment status | | `GET /api/l402/challenges/{paymentHash}` | Look up one challenge | | `POST /api/l402/challenges/verify` | Verify a macaroon + preimage | | `POST /api/l402/challenges/verify-credential` | Verify a modern `Payment` bearer token (single-use) | | `GET /api/proxy/{proxyId}/verification` | Read a proxy's [ownership verification](#proxy-ownership-verification) state and instructions | | `POST /api/proxy/{proxyId}/verification/token` | Issue (or re-issue) a proxy's verification token, shown once | | `POST /api/proxy/{proxyId}/verify` | Check the target for the token now | | `GET /api/merchant/subscription/renew` | Read your [renewal quote](#producer-renewal), with the full calculation for a producer account | | `GET /api/producer/claim/status` | Read your claimed proxy, paid-through date, and [current renewal](#claim-status) | ## Base URL ``` https://api.lightningenable.com ``` ## Authentication Every request requires your Lightning Enable merchant API key in the `X-API-Key` header. Find or regenerate it at **Dashboard → Settings → API Credentials**. ```http X-API-Key: ``` The producer API is available on every plan. The Free Producer Sandbox caps it at 3 endpoints, 200 challenges a month, and 1,000 sats per challenge; Agentic Commerce ($49/mo) and Business ([contact us](mailto:support@lightningenable.com)) remove those caps. Your account also needs a payment provider (Strike, OpenNode, or an NWC wallet) before it can mint. ## `POST /api/l402/challenges` Mint a Lightning invoice and macaroon for a given resource. Returns the components of a 402 Payment Required challenge that you present to the caller. ### Request ```http POST /api/l402/challenges HTTP/1.1 Host: api.lightningenable.com X-API-Key: Content-Type: application/json Idempotency-Key: { "resource": "/api/premium/weather", "priceSats": 100, "description": "Premium weather forecast" } ``` **Body:** | Field | Type | Required | Notes | |---|---|---|---| | `resource` | string (≤ 848 chars) | yes | The path/resource the challenge is for. Bound as a caveat in the macaroon — the resulting token is only valid for this resource. Longer values are rejected with `400` before any invoice is created. | | `priceSats` | integer (≥ 1) | yes | Price in satoshis. | | `description` | string (≤ 500 chars) | no | Embedded in the Lightning invoice; visible to the payer in their wallet UI. Longer values are rejected with `400`. The string that reaches the invoice is truncated to **200 UTF-8 bytes**, ending in `…`, to stay inside the BOLT11 description field and the payment providers' own limits — so keep anything the payer needs to read at the front. Omit the field, or send a blank one, and you get `L402 access: {resource}`, truncated the same way. | | `idempotencyKey` | string (≤ 200 chars) | no | Same meaning as the `Idempotency-Key` header, for clients that can't set headers. The header wins if you send both. See [Idempotency](#idempotency). | **Headers:** | Header | Required | Notes | |---|---|---| | `X-API-Key` | yes | Merchant API key | | `Content-Type` | yes | `application/json` | | `Idempotency-Key` | no | If supplied, the same challenge is returned for repeat calls with the same key for the life of that invoice. At most 200 characters — a longer key is a `400`, never a truncation. See [Idempotency](#idempotency). | | `X-Idempotency-Key` | no | The spelling this API shipped with. Still accepted and identical in behaviour; `Idempotency-Key` wins if you send both. | ### Response — 200 OK ```json { "invoice": "lnbc1u1p3...", "macaroon": "AgELbWFjYXJvb24=...", "paymentHash": "abc123...", "expiresAt": "2026-05-12T01:00:00Z", "resource": "/api/premium/weather", "priceSats": 100, "mppChallenge": "Payment id=\"k9Q3...\", realm=\"lightning-enable\", method=\"lightning\", intent=\"charge\", request=\"eyJhbW91bnQiOi...\", expires=\"2026-05-12T01:00:00Z\", invoice=\"lnbc1u1p3...\", amount=\"100\", currency=\"sat\"" } ``` | Field | Type | Notes | |---|---|---| | `invoice` | string | BOLT11 Lightning invoice the caller must pay | | `macaroon` | string | **URL-safe base64** (base64url) macaroon containing the payment hash and caveats. Uses `-`/`_` instead of `+`/`/` and may omit padding — decode with a base64url-aware function (`base64.urlsafe_b64decode` in Python, `Buffer.from(s, 'base64url')` in Node, `WebEncoders.Base64UrlDecode` in .NET) rather than standard base64. | | `paymentHash` | string | Hex payment hash linking the macaroon to the invoice | | `expiresAt` | string (ISO 8601) | When the Lightning invoice expires | | `resource` | string | Echoes the request's `resource` | | `priceSats` | integer | Echoes the request's `priceSats` | | `mppChallenge` | string \| null | Ready-to-emit `WWW-Authenticate: Payment ...` value for the same invoice. It carries both the modern draft-00 parameters (`id`, `realm`, `method`, `intent`, `request`, `expires`) and the legacy `invoice` / `amount` / `currency` parameters, so any `Payment`-scheme client can use it. Serve it alongside the `L402` header. `null` only when MPP is switched off at the service level (it is on for the hosted API). See [Payment (MPP) credentials](/api-reference/l402#payment-mpp-credentials). | ### Error responses Errors on this endpoint are [RFC 9457 problem documents](#error-format) — `application/problem+json`, with a stable `type` URI to branch on and the pre-RFC `error` / `message` members still present. | Status | `type` suffix | Meaning | |---|---|---| | `400` | `invalid_request` | The body failed validation: missing `resource`, `resource` over 848 characters, `priceSats` missing, below 1, fractional, or not a number, or a body that isn't valid JSON. Carries per-field `errors` and an `example` body. When your plan caps the price, the `priceSats` message quotes that maximum. | | `400` | `invalid_idempotency_key` | The key was blank or longer than 200 characters. | | `400` | `invalid_resource` | `resource` exceeds the 848-character limit. Carries `max_length`. | | `400` | `reserved_resource` | `resource` starts with a prefix reserved for Lightning Enable's own flows (`renew:`, `signup:`). | | `400` | `payment_provider_not_configured` | No payment provider credential (Strike key, OpenNode key, or NWC connection) on your account, so no invoice could be created. Carries `docs`. This is the most common refusal on a brand-new account, and it's checked before any plan cap. | | `400` | `challenge_creation_failed` | Another business-logic refusal from the challenge service. | | `401` | `authentication_required` / `merchant_not_found` | Missing, malformed, or revoked `X-API-Key`. | | `402` | `plan_price_limit` / `plan_volume_limit` / `plan_endpoint_limit` | A plan cap was reached. Each carries `current_plan` and `upgrade_url`; `plan_price_limit` also carries `max_price_sats` (lower the price or upgrade). Plan caps are checked only after `payment_provider_not_configured`. | | `403` | — | L402 is not enabled on your plan. Body includes `current_plan` and `action_required: "upgrade_plan"`. | | `409` | `idempotency_key_reuse` | The key was already used for a different `resource` or `priceSats`. Carries `bound_resource` / `bound_price_sats` and `requested_resource` / `requested_price_sats`. | | `409` | `endpoint_retired` | The resource was retired to free a plan slot and can no longer mint. | | `415` | `unsupported_media_type` | No body, or a `Content-Type` other than `application/json`. The `detail` shows an example body. | | `429` | — | Rate-limited. | | `502` | `provider_rejected` | Your payment provider refused to create the invoice, usually an invalid key or a key without invoice permission. Carries `provider`, `upstreamStatus`, and the fix in `detail`. | | `503` | `provider_unavailable` | Your payment provider returned a 5xx. Retry shortly. | | `503` | `payment_provider_busy` | Your NWC wallet already has the maximum number of requests in flight. Nothing was invoiced; retry after `Retry-After`. | | `503` | `challenge_persist_failed` | The challenge could not be durably recorded, so none was issued — **nothing was invoiced**. Safe to retry. | --- ## `GET /api/l402/challenges` List the challenges you have minted, newest first, with their payment status. Scoped to the account behind your API key — there is no tenant parameter to pass and no way to see anyone else's. ### Request ```http GET /api/l402/challenges?status=paid&since=2026-09-01T00:00:00Z&limit=50&offset=0 HTTP/1.1 Host: api.lightningenable.com X-API-Key: ``` | Query parameter | Type | Default | Notes | |---|---|---|---| | `status` | `paid` \| `unpaid` \| `expired` | none | Omit to list everything. Any other value is a `400`. | | `since` | ISO 8601 timestamp | none | Lower bound on `createdAt`. Send an offset (`Z` or `+02:00`); a bare timestamp is read as UTC. | | `limit` | integer | `50` | Clamped to 1..200 rather than rejected. | | `offset` | integer | `0` | Clamped to at least 0. | ### Response — 200 OK The total matching your filter, ignoring paging, is also in the `X-Total-Count` header. ```json { "challenges": [ { "paymentHash": "abc123...", "resource": "/api/premium/weather", "amountSats": 100, "status": "paid", "createdAt": "2026-09-05T18:00:00Z", "paidAt": "2026-09-05T18:00:41Z", "expiresAt": "2026-09-05T19:00:00Z", "idempotencyKey": "req-abc-123" } ], "total": 1, "limit": 50, "offset": 0, "status": "paid", "since": null } ``` | Field | Notes | |---|---| | `paymentHash` | Hex payment hash of the challenge's invoice. The correlation handle — safe to log and store. | | `status` | `paid` once a credential from this challenge has verified; `unpaid` while the token window is open; `expired` after it closes without proof of payment. `paid` is permanent — a challenge paid inside its window never reverts to `expired`. | | `paidAt` | When payment was first **proven** — the first successful verification of a credential from this challenge. `null` while unproven, which includes an invoice that was paid but whose credential you have never presented back. | | `expiresAt` | End of the token window; matches the macaroon's `expires` caveat. `null` on rows minted before this was recorded. | | `idempotencyKey` | The key the challenge was minted under, if you sent one. | :::note What this is not `paidAt` is proof-of-payment, not a settlement record. Lightning Enable does not hold funds — the sats settled with your payment provider the moment the invoice was paid, which may be earlier than this timestamp. Your provider's dashboard remains the record of what you were paid. ::: Never returned: the macaroon and the preimage. A preimage is bearer money; the payment hash is what you correlate on. ### Error responses | Status | `type` suffix | Meaning | |---|---|---| | `400` | `invalid_status_filter` | `status` was not one of the three values. Carries `allowed_status`. | | `401` | `authentication_required` | Missing or invalid `X-API-Key`. | --- ## `GET /api/l402/challenges/{paymentHash}` Look up a single challenge you minted. ```http GET /api/l402/challenges/abc123... HTTP/1.1 X-API-Key: ``` Returns the same object as one element of the list above. ### Error responses | Status | `type` suffix | Meaning | |---|---|---| | `400` | `invalid_payment_hash` | Not 64 hexadecimal characters. | | `401` | `authentication_required` | Missing or invalid `X-API-Key`. | | `404` | `challenge_not_found` | You have no challenge with that payment hash. A hash belonging to another account returns this same `404` — never a `403`, which would confirm the hash exists. | --- ## `POST /api/l402/challenges/verify` Verify an L402 credential — a macaroon + preimage pair presented in an `Authorization: L402` header from a caller who paid your challenge. ### Request ```http POST /api/l402/challenges/verify HTTP/1.1 Host: api.lightningenable.com X-API-Key: Content-Type: application/json { "macaroon": "AgELbWFjYXJvb24=...", "preimage": "deadbeef..." } ``` **Body:** | Field | Type | Required | Notes | |---|---|---|---| | `macaroon` | string | required for L402 | The URL-safe base64 (base64url) macaroon from the caller's `Authorization` header — pass through unchanged, no re-encoding. Omit only if doing MPP-style preimage-only verification (and MPP is enabled on your account). | | `preimage` | string (hex, 64 chars) | yes | The payment preimage proving the invoice was paid. | | `resource` | string \| null | recommended | The path the caller is gating. **If you provide it, the producer API enforces the macaroon's `path` caveat against this value** — a mismatch returns `valid: false`. If you omit it, the path caveat is read out but not enforced (the integrator is responsible for the comparison). | | `amountSats` | integer \| null | recommended | The price tier the gated endpoint requires. **If you provide it, the producer API enforces the macaroon's `amount_sats` caveat against this value** — prevents replaying a cheap token against an expensive endpoint matched by a wildcard rule. If you omit it, the amount caveat is read out but not enforced. | :::tip Defense in depth Two enforcement guarantees are **always** applied server-side regardless of which optional fields you pass: - **Authenticated merchant_id is always compared to the macaroon's `merchant_id` caveat.** Calling the verify endpoint as merchant B with a token bound to merchant A returns `valid: false`. There is no opt-out — this is the cross-tenant IDOR guard. - **Macaroon signature, preimage hash, and `expires` caveat are always verified.** The optional `resource` and `amountSats` fields opt you into additional path/amount caveat enforcement. Pass them whenever you have the values handy; the only reason to skip is a generic verifier that doesn't know the gated path up front. ::: ### Response — 200 OK The producer API returns **200 OK for both valid and invalid tokens** — read the `valid` field rather than relying on the status code. **Valid token:** ```json { "valid": true, "resource": "/api/premium/weather", "merchantId": 42, "amountSats": 100, "paymentHash": "abc123..." } ``` **Invalid token:** ```json { "valid": false, "error": "Invalid preimage" } ``` | Field | Type | Notes | |---|---|---| | `valid` | bool | The gate. Inspect this. | | `error` | string \| null | Failure reason; only populated when `valid: false`. Examples: `"Invalid preimage"`, `"Token bound to a different resource"`, `"Macaroon signature invalid"`. | | `resource` | string \| null | The path/resource the token is bound to (from the macaroon's caveat). Assert this matches the resource the caller is actually requesting. | | `merchantId` | integer \| null | The merchant ID the macaroon was issued under. | | `amountSats` | integer \| null | The amount the token was issued for. | | `paymentHash` | string \| null | The payment hash from the macaroon's identifier. | ### Error responses Also [RFC 9457 problem documents](#error-format). | Status | `type` suffix | Meaning | |---|---|---| | `400` | `invalid_verification_request` | A field was present but unusable — a blank `macaroon`, a blank `resource`, `amountSats` below 1. | | `400` | `mpp_not_supported` | Preimage-only verification requested but MPP is not enabled. | | `401` | `authentication_required` / `merchant_not_found` | Missing or invalid `X-API-Key`. | | `403` | — | L402 not enabled on your plan. | Note: a *valid macaroon* with an *invalid preimage* still returns `200 OK` with `valid: false`. Non-2xx is reserved for auth / plan / transport problems. **Side effect:** the first successful verification of a credential marks the underlying challenge paid and fires [`l402.challenge.paid`](#payment-webhooks). Later verifications of the same credential do not re-fire it. --- ## `POST /api/l402/challenges/verify-credential` Verify a **modern `Payment` bearer credential**: the `Authorization: Payment ` token defined by `draft-httpauth-payment-00` + `draft-lightning-charge-00`. Use `/verify` for classic L402 tokens and legacy `Payment method="lightning", preimage="..."` credentials; use this endpoint for the bearer form. Format details: [Payment (MPP) credentials](/api-reference/l402#payment-mpp-credentials). **This endpoint consumes the credential.** A modern credential is single-use by design: the first successful verification marks it consumed atomically, and a second call with the same token returns `valid: false`. Verify once, then serve the resource. ### Request ```http POST /api/l402/challenges/verify-credential X-API-Key: YOUR_MERCHANT_API_KEY Content-Type: application/json ``` | Field | Type | Required | Notes | |---|---|---|---| | `credential` | string | Yes | The token as received. The leading `Payment ` scheme word is optional. | | `resource` | string | No | When set, the challenge must have been minted for this resource. | | `amountSats` | integer | No | When set, the challenge must have been minted for exactly this price. | ### Response — 200 OK ```json { "valid": true, "consumed": true, "resource": "/api/premium/weather", "merchantId": 42, "amountSats": 100, "paymentHash": "abc123...", "receipt": "eyJjaGFsbGVuZ2VJZCI6Ims5UTMuLi4iLC..." } ``` | Field | Notes | |---|---| | `valid` | `true` when the preimage matches, the challenge binding is intact, the credential is unexpired, and it had not been consumed before. | | `consumed` | `true` when this call consumed the credential. | | `receipt` | base64url(JCS) receipt: `{"challengeId","method":"lightning","reference":"","status":"success","timestamp"}`. Return it to the payer as the `Payment-Receipt` response header. The reference is the payment hash, never the preimage. | | `error` | Present when `valid` is `false`. A static description, never the token or preimage. | ### Error responses | Status | `type` suffix | Meaning | |---|---|---| | `400` | `mpp_not_supported` | Modern credentials are switched off on this server. | | `401` | `authentication_required` / `merchant_not_found` | Missing or invalid `X-API-Key`. | | `403` | `l402_not_enabled` | L402 not enabled on your plan. | A malformed or already-consumed token returns `200 OK` with `valid: false`, the same convention as `/verify`. **Side effect:** the first successful verification marks the underlying challenge paid and fires [`l402.challenge.paid`](#payment-webhooks). --- ## Proxy ownership verification A proxy can be listed publicly only after you prove that you control its target API. These three endpoints issue the proof token, run the check, and report the state. The task-by-task guide, with copy-paste examples for each method and troubleshooting, is [Verify ownership of your API](./verify-ownership). All three take your merchant API key in `X-API-Key`. A proxy that belongs to another account answers `404`, the same as a proxy that doesn't exist. Verification never changes what the proxy does for callers; it changes only whether the proxy can be listed (and, for any unverified proxy of an account created with the one-payment claim, lifts its unverified challenge limit, which refuses callers past it with `429` and `"error": "unverified_proxy_rate_limited"`). Changing a proxy's target URL clears its verification and unlists it, whatever the method except `platform`; the token stays valid for the new target. ### `GET /api/proxy/{proxyId}/verification` Returns the proxy's verification state and the exact instructions for publishing a token. Never returns the token. Rate limit: the standard merchant-key limit. ```http GET /api/proxy/forecast-api-a1b2/verification HTTP/1.1 Host: api.lightningenable.com X-API-Key: ``` #### Response — 200 OK For a proxy whose target base URL is `https://api.example.com/v1`: ```json { "proxyId": "forecast-api-a1b2", "verified": false, "method": null, "verifiedAt": null, "lastCheckedAt": null, "failures": 0, "tokenIssued": true, "token": null, "listed": false, "nextCheckAt": null, "checkedAt": null, "reason": null, "pending": null, "retryAfterSeconds": null, "instructions": { "tokenPlaceholder": "", "methods": [ { "method": "well-known", "order": 1, "summary": "Serve this JSON over HTTPS at the target host's /.well-known/lightning-enable.json. For several proxies on one host, \"verify\" can be an array of tokens.", "url": "https://api.example.com/.well-known/lightning-enable.json", "recordName": null, "recordType": null, "headerName": null, "body": "{\"verify\":\"\"}", "value": "" }, { "method": "dns-txt", "order": 2, "summary": "Add a DNS TXT record with the token as its value.", "url": null, "recordName": "_lightning-enable.api.example.com", "recordType": "TXT", "headerName": null, "body": null, "value": "" }, { "method": "header", "order": 3, "summary": "Return the X-Lightning-Enable-Verify response header from the target's base URL (its path, read without any query string) on HEAD or GET, with any status except a redirect.", "url": "https://api.example.com/v1/", "recordName": null, "recordType": null, "headerName": "X-Lightning-Enable-Verify", "body": null, "value": "" } ], "tokenEndpoint": "/api/proxy/forecast-api-a1b2/verification/token", "verifyEndpoint": "/api/proxy/forecast-api-a1b2/verify", "note": "Publish the token by any one method, replacing , then call POST /api/proxy/forecast-api-a1b2/verify. POST /api/proxy/forecast-api-a1b2/verification/token issues a new token (shown once) and replaces the old one. Redirects are not followed. A verified proxy is re-checked daily and can then be listed publicly." } } ``` | Field | Meaning | |---|---| | `verified` | `true` while the proxy is verified by `well-known`, `dns-txt`, `header`, or `platform`. `false` for a `grandfathered` proxy, which is listed because it predates verification, not because ownership was proven. | | `method` | How it was verified: `well-known`, `dns-txt`, or `header`; or `grandfathered` for a proxy that was listed before verification existed, or `platform` for a Shopify store entry that Lightning Enable operates on its own host (both never re-checked, kept listed; a target URL change drops `grandfathered`). `null` while unverified. | | `verifiedAt` | When the current method first verified the proxy (verified since); later successful checks don't move it. `null` while unverified. | | `lastCheckedAt` | When a check last completed, by you or by the daily re-check. | | `failures` | Consecutive failed checks. Three, found by the daily re-check, make a verified proxy unverified. | | `tokenIssued` | Whether a token exists. The token itself can't be read back. | | `token` | Always `null` here. Only the token endpoint returns it, once. | | `listed` | Whether the proxy is publicly listed now. | | `nextCheckAt` | When the daily re-check next runs. `null` when the proxy isn't re-checked: while it's unverified or inactive, and for `grandfathered` and `platform` proxies. | | `checkedAt`, `reason`, `pending`, `retryAfterSeconds` | Set only by `POST .../verify`. | | `instructions.methods` | The three methods, in the order the check tries them. Replace `tokenPlaceholder` with your token in `body` or `value`. A target URL with no usable host lists only `header`. | ### `POST /api/proxy/{proxyId}/verification/token` Issues a new token and returns the state above with `token` set. **This response is the only time the token is shown**; only its hash is stored, and the response carries `Cache-Control: no-store`. The token is 32 random bytes, base64url-encoded (43 characters). Re-issuing replaces the old token, which stops working at once. The proxy becomes unverified and is unlisted (including its Nostr listing) until you publish the new token and verify again. Rate limit: 10 requests per minute per API key. ```bash curl -X POST https://api.lightningenable.com/api/proxy/forecast-api-a1b2/verification/token \ -H "X-API-Key: $LIGHTNING_ENABLE_API_KEY" ``` ### `POST /api/proxy/{proxyId}/verify` Runs the check now and returns the state. The check looks for the current token at the well-known file, then the DNS TXT record, then the response header (`HEAD`, then `GET`), and stops at the first match. It sends requests only to public hosts and never follows a redirect. Rate limit: 10 requests per minute per API key, shared with the token endpoint. ```bash curl -X POST https://api.lightningenable.com/api/proxy/forecast-api-a1b2/verify \ -H "X-API-Key: $LIGHTNING_ENABLE_API_KEY" ``` The response fills four more fields of the state: | Field | Meaning | |---|---| | `checkedAt` | When this check ran. | | `reason` | When the check didn't verify: what each method saw, as `well-known: ...; dns-txt: ...; header: HEAD ...; GET ...`. Never contains the token. `null` on success. | | `pending` | `true` when the check ran past its overall time limit. Nothing was recorded, so it doesn't count as a failure; `reason` says what the check learned before it stopped. | | `retryAfterSeconds` | With `pending: true`: how long to wait before calling `verify` again. Also sent as a `Retry-After` header. | A failed check returns `200` with `verified: false`, not an error status. #### Error responses | Status | Body | Meaning | |---|---|---| | `401` | | Missing or invalid `X-API-Key`. | | `404` | `{"error": "Proxy not found"}` | No proxy with this ID in your account. | | `409` | problem+json, `type` suffix `verification_token_required`, with `tokenEndpoint` | No token was issued for this proxy. Call the token endpoint first. | | `429` | | More than 10 `POST` calls in a minute on this API key. | The token endpoint returns the same `401`, `404`, and `429` responses. ### The `verification` block in the claim response A proxy created with the one-payment claim (`POST /api/producer/claim`) gets a token at creation, so your agent can verify without another call. The claim's `200` response carries (other fields omitted): ```json { "listing": { "requested": true, "listed": false, "reason": "verification_required" }, "verification": { "token": "", "instructions": { "tokenPlaceholder": "", "methods": ["..."], "tokenEndpoint": "...", "verifyEndpoint": "...", "note": "..." }, "verifyEndpoint": "/api/proxy/forecast-api-a1b2/verify" } } ``` - `verification.token` is the raw token, shown in this response only. A replay of the same claim returns `"token": null`; issue a new one with `POST /api/proxy/{proxyId}/verification/token`. - `verification.instructions` has the same shape as `instructions` in the state above. - `listing.reason` stays `verification_required` and `listing.listed` stays `false` until the proxy is verified and you turn on listing. ### Listing an unverified proxy `PUT /api/proxy/{proxyId}/manifest/settings` with `"manifestPubliclyListed": true` on an unverified proxy answers `422`: ```json { "error": "ownership_verification_required", "message": "Public registry listings must be ownership-verified. ...", "verify_endpoint": "/api/proxy/forecast-api-a1b2/verify" } ``` Verify, then send the same request again. ### Verification in the manifest and registry A proxy's manifest (`service` block) and its public registry entry carry `verified`, `verification_method`, `verified_at` (verified since), and `last_checked_at`. Only verified proxies, including grandfathered ones, appear in the public registry, agent discovery, and Nostr. A grandfathered listing shows `verified: false` with `verification_method: "grandfathered"`: it's listed because it predates verification, and nothing was proven. The proxy object returned by `GET /api/proxy/{proxyId}` also carries `verification: { verified, method, lastCheckedAt }`, never the token. --- ## Producer renewal A producer account, which is an account created with the one-payment claim, renews at producer pricing: a base price in dollars that includes one active proxy, plus an increment for each additional active proxy, converted to sats at the live rate when the quote or challenge is issued and kept between a configured minimum and maximum. When no rate is available, the minimum applies. [How producer pricing works](./producer-pricing) explains the structure. Every other account renews at the shared sats price, and its quote has no `breakdown` block. Pay the renewal with the L402 flow on `POST /api/merchant/subscription/renew`, described in [Renew in sats](/getting-started/activate-with-lightning#renew-in-sats). For a producer account, the token you pay is honored at the sats amount it was issued for, even if the price changes before you pay. Every other account keeps the shared rule in the [Merchant Settings API](/api-reference/merchant-settings#post-complete-the-renewal): a token minted before a price increase is refused. ### Renewal quote `GET /api/merchant/subscription/renew` with your API key returns the quote. It creates no invoice. For a producer account it adds `breakdown` (other fields as in the [Merchant Settings API](/api-reference/merchant-settings#renew-with-lightning-l402)): ```json { "eligible": true, "reason": null, "priceSats": , "periodDays": , "paymentMethod": "l402", "currentPeriodEnd": "2026-12-10T14:03:22Z", "periodEndIfPaid": "2027-01-09T14:03:22Z", "endpoint": "/api/merchant/subscription/renew", "breakdown": { "baseUsdCents": , "perProxyUsdCents": , "includedProxies": 1, "activeProxies": 3, "billableExtraProxies": 2, "usdCentsTotal": , "btcUsd": , "sats": , "pricedAt": "2026-11-08T09:12:40Z" } } ``` | Field | Meaning | |---|---| | `baseUsdCents` | The base price, in US cents. It covers `includedProxies` active proxies. | | `perProxyUsdCents` | The increment for each active proxy beyond `includedProxies`, in US cents. | | `includedProxies` | How many active proxies the base price covers. | | `activeProxies` | Your proxies that are active now. Inactive proxies don't count. | | `billableExtraProxies` | `activeProxies` minus `includedProxies`, never below zero. | | `usdCentsTotal` | `baseUsdCents` plus `billableExtraProxies` times `perProxyUsdCents`. | | `btcUsd` | The exchange rate used for the conversion. `null` when no rate was available; the price is then the configured sats minimum. | | `sats` | `usdCentsTotal` converted to sats and kept within the configured minimum and maximum. Equal to `priceSats`. | | `pricedAt` | When the quote was priced (UTC). | Sats figures here are placeholders: read them from the live quote. When `eligible` is `false` with `reason: "PrepaidCapReached"`, you've prepaid as far ahead as a producer account can; ask again closer to your paid-through date. ### Claim status `GET /api/producer/claim/status` with your API key returns your newest claimed proxy (`lane`, `status`, `proxyActive`, `proxyId`, `proxyUrl`, `manifestUrl`, `paidThrough`, `recovery`) and a `renew` block: | Field | Meaning | |---|---| | `renew.endpoint` | The renew endpoint, `/api/merchant/subscription/renew`. | | `renew.periodDays` | Days one renewal adds to `paidThrough`. | | `renew.quoteSats` | What one renewal costs this account now, in sats. For a producer account, it counts the current active proxies; any other account gets the shared sats price. | | `renew.usdCents` | The same price in US cents before conversion. `null` for an account that isn't a producer account. | | `renew.activeProxies` | The active proxies the price counts. | The `200` response from `POST /api/producer/claim` also carries `renew.quoteSats`, but that figure is the one-proxy price. This status endpoint is authoritative for your current proxy count and price. An account with no claimed proxy gets `404` with `"error": "no_claim"`. ### Renewal in the manifest The JSON manifest of a proxy owned by a producer account (`/l402/proxy/{proxyId}/.well-known/l402-manifest.json`) carries two extra fields in its `service` block, so a buyer's agent can see a lapse coming: ```json { "service": { "paid_through": "2026-12-10T14:03:22Z", "renewal": { "endpoint": "/api/merchant/subscription/renew", "period_days": } } } ``` | Field | Meaning | |---|---| | `paid_through` | When the owner's paid period ends, in ISO 8601 UTC to whole seconds. Omitted when the account has no paid-through date. | | `renewal.endpoint` | Where the owner renews. | | `renewal.period_days` | Days one renewal adds. | The manifest carries no price. Manifests of proxies owned by other accounts don't have these fields. --- ## Error format Every error the producer API returns itself is `application/problem+json` ([RFC 9457](https://www.rfc-editor.org/rfc/rfc9457.html)): ```json { "type": "https://lightningenable.com/problems/idempotency_key_reuse", "title": "Idempotency key reuse", "status": 409, "detail": "This idempotency key was already used for a different resource or price. …", "error": "idempotency_key_reuse", "message": "This idempotency key was already used for a different resource or price. …", "bound_resource": "/api/premium/weather", "bound_price_sats": 100 } ``` **Branch on `type`.** It is a stable identifier that never changes meaning, and it is safe to switch on in code. `title` and `detail` are prose written for a human reading a log, and may be reworded. `error` and `message` are the pre-RFC members, kept so integrations written against the older shape keep working — `error` always equals the `type` suffix, and `message` always equals `detail`, except on a handful of paths that shipped a different `error` string before RFC 9457 and keep it verbatim. Endpoint-specific members (`current_plan`, `max_length`, `docs`, `bound_price_sats`, …) sit alongside them. `type` URIs are identifiers, not URLs to fetch — nothing is served at them. --- ## Token reuse within the validity window L402 tokens remain valid for **repeated** verifications until the macaroon's `expires` caveat passes, **60 minutes** from issuance by default. During that window `/verify` returns `valid: true` for every presentation of a valid macaroon + preimage pair, including replays. This is intentional. Two separate durations: - **Token validity (60 minutes by default)** — the macaroon's `expires` caveat. The window during which a *paid* token can be re-presented. - **Invoice expiry (10 minutes by default)** — the window during which the Lightning invoice itself can be paid. After it, the invoice is dead and the macaroon is useless. Caveat enforcement on `POST /api/l402/challenges/verify`: - **`merchant_id` caveat** — **always enforced** against the authenticated merchant id (derived from your `X-API-Key`). Merchant A cannot verify a macaroon that was bound to merchant B. No opt-out. - **`path` caveat** — **enforced when you pass `resource` in the verify request body**. Without `resource`, the path caveat is reported in the response (`resource` field) but not compared — the integrator is responsible for the check. - **`amount` caveat** — **enforced when you pass `amountSats` in the verify request body**. Without `amountSats`, the amount is reported in the response but not compared. - **`expires` caveat** — always enforced. A token presented after its validity window returns `valid: false`. Caveats do not prevent same-resource reuse within the validity window. That's by design: a paid agent making many quick calls within one paid window is a legitimate use case. **If you specifically need single-use semantics** for a particular endpoint (e.g., a one-shot model that returns expensive state), track consumed preimages locally in your handler. A trivial in-memory set keyed on `paymentHash` works for single-process apps; Redis or your existing database works for distributed deployments. The verification result includes `paymentHash` precisely so you can do this without re-parsing the macaroon. --- ## Idempotency A retry must never produce a second payable invoice. Send an `Idempotency-Key` on `POST /api/l402/challenges` and you get the same challenge back — same invoice, same macaroon, same payment hash — for the life of that invoice: ```http POST /api/l402/challenges Idempotency-Key: req-abc-123 { "resource": "/api/premium/weather", "priceSats": 100 } ``` A replayed response carries `X-Idempotency-Replayed: true`. The body is byte-identical to the first one, so a client that ignores the header sees exactly what it saw before. The key is recorded on the challenge itself, not in a cache, so the replay survives a deploy, a restart, and a load balancer sending your retry to a different instance. It is scoped to your account: two merchants can use the same key string without colliding. **Rules:** - **Same key, same `resource` and `priceSats`** → the original challenge, replayed. `description` is not part of the match, so changing only the description still replays. - **Same key, different `resource` or `priceSats`, while the original invoice is still live** → `409` with `type: .../idempotency_key_reuse`. One key means one live charge; guessing which of the two you meant risks handing you an invoice for the wrong amount. Use a fresh key for a new charge. - **Key whose invoice has expired** → the key is released and a fresh challenge is minted under it, **whatever you ask for**. An expired binding holds no payable invoice, so there is nothing left to conflict with: reusing a spent key at a different price is a normal mint, not a `409`. Invoices are payable for 10 minutes by default. - **Two requests at once with one key** → exactly one mints. The other gets that same challenge if it asked for the same charge, or the same `409` it would have got from a sequential retry if it asked for a different one. Either way you are never charged twice for a race. - **Blank, or longer than 200 characters** → `400`. The key is never truncated: truncating would collapse two distinct keys onto one challenge, which is the exact double-charge the key exists to prevent. This means a key derived from the work you are doing — `order-9182-challenge` — keeps working across the whole life of that order: it replays inside the invoice window and mints fresh after it, and it is never poisoned by a price change. Clients that can't set headers can send `idempotencyKey` in the body instead. `X-Idempotency-Key` — the spelling this API shipped with — still works and behaves identically. **If you send no key**, every call mints a fresh challenge with its own invoice. For native middleware, choose between two trade-offs. With no key selector, every unauthenticated request to a paid route mints a fresh invoice, and each one counts toward the Free Producer Sandbox monthly challenge cap, so a crawler can use the cap up. With a key selector, each key gets one invoice per route and price for the life of that invoice, so the key must identify the actual buyer, such as a session or order id, and never a bare IP address that many buyers can share behind NAT or a load balancer. Include the price in the key when your pricing is dynamic, because the same key at a different price is a `409`, and keep the key within 200 characters. `POST /api/l402/challenges/verify` is a read-only check on the macaroon + preimage. Repeated verification of the same pair during the token validity window returns the same `valid: true` result every time — see [Token reuse within the validity window](#token-reuse-within-the-validity-window) above. (The first one also marks the challenge paid; see [Payment webhooks](#payment-webhooks).) --- ## Payment webhooks When a challenge you minted is first proven paid, Lightning Enable POSTs `l402.challenge.paid` to your account's callback URL. Set it with [`PUT /api/merchant/webhook-url`](/api-reference/merchant-settings#update-webhook-url) (the dashboard has no webhook field); with no callback configured, nothing is sent and nothing is queued. ```json { "event": "l402.challenge.paid", "paymentHash": "abc123...", "resource": "/api/premium/weather", "amountSats": 100, "paidAt": "2026-09-05T18:00:41Z", "idempotencyKey": "req-abc-123" } ``` Signed with the same `X-LightningEnable-Signature: t={timestamp},v1={hmac_sha256}` scheme and delivered by the same retrying forwarder as every other Lightning Enable webhook — see [Webhooks](../../api-reference/webhooks) for signature verification and retry behaviour. **"Paid" means proven, not settled.** The event fires the first time a credential minted from that challenge verifies successfully — through `/verify`, `/verify-credential`, or an L402-gated proxy request. The payer can only hold that credential by having settled the invoice with your payment provider, so it is proof of payment; but the sats moved when the invoice was paid, which may be moments earlier. Lightning Enable does not hold funds, and your provider remains the record of what you were paid. It fires **once per challenge**. L402 tokens stay valid for repeated use until their `expires` caveat, so the same credential is verified many times per paid invoice; only the first transition notifies. An invoice that was paid but whose credential is never presented back to Lightning Enable produces no event — there is nothing to prove it. Poll `GET /api/l402/challenges?status=unpaid` if you need to reconcile those against your provider. Each delivery attempt signs the same body with a fresh timestamp, so dedupe on `paymentHash`, never on the signature value. ### Subscription expiring webhook Lightning Enable POSTs `l402.subscription.expiring` to your callback URL about seven days and about one day before your paid-through date, so your agent can renew before authenticated calls start answering `403` with `action_required: "renew_subscription"`. It goes to every account that pays for its plan in sats, has a callback URL, and can renew now (its renewal quote says `eligible: true`). It needs no email address. ```json { "event": "l402.subscription.expiring", "paidThrough": "2026-12-10T14:03:22Z", "daysLeft": 7, "renewEndpoint": "/api/merchant/subscription/renew", "quoteSats": , "periodDays": , "idempotencyKey": "4821:expiring:20261210:7" } ``` | Field | Meaning | |---|---| | `event` | Always `l402.subscription.expiring`. | | `paidThrough` | When your paid period ends (UTC). | | `daysLeft` | Whole UTC days from the day of the notice to `paidThrough`. A notice on the last day says `0`. | | `renewEndpoint` | Where to renew. | | `quoteSats` | What a renewal costs now, the same figure as `priceSats` in the [renewal quote](#renewal-quote). Read the quote again before you pay; the price can move with the exchange rate. | | `periodDays` | Days one renewal adds. | | `idempotencyKey` | `{accountId}:expiring:{paidThrough as yyyyMMdd}:{slot}`, where the slot is `7` for the early notice and `1` for the final notice. Unique per account, period end, and slot. Dedupe on it. | Sats and day figures in the example are placeholders: read the live price from the [renewal quote](#renewal-quote). If a daily run is missed, the next run catches up, so a notice can arrive later than its slot: a catch-up early notice can carry `daysLeft` 5 with a key ending in `:7`. Use `daysLeft` for the time remaining and the key only to dedupe. It's signed and retried like `l402.challenge.paid`. Each notice is sent once per period end. Renewing moves your paid-through date, so the next period gets its own notices. With no callback URL, nothing is sent. --- ## End-to-end example flows ### Mint + present a challenge ```bash # 1. Caller requests your endpoint without paying $ curl -i https://your-api.example/api/premium/weather HTTP/1.1 402 Payment Required WWW-Authenticate: L402 macaroon="AgEL...", invoice="lnbc1u..." Content-Type: application/json { "error": "Payment Required", "l402": { ... } } ``` In your handler before responding with that 402, you called: ```bash $ curl -X POST https://api.lightningenable.com/api/l402/challenges \ -H 'X-API-Key: $LIGHTNING_ENABLE_API_KEY' \ -H 'Content-Type: application/json' \ -d '{"resource":"/api/premium/weather","priceSats":100}' { "invoice": "lnbc1u1p3...", "macaroon": "AgEL...", "paymentHash": "abc123", "expiresAt": "2026-05-12T01:00:00Z", "resource": "/api/premium/weather", "priceSats": 100 } ``` ### Verify a returning request ```bash # Caller pays the invoice, gets the preimage, retries with credential: $ curl -i https://your-api.example/api/premium/weather \ -H 'Authorization: L402 AgEL...:deadbeef...' ``` In your handler: ```bash $ curl -X POST https://api.lightningenable.com/api/l402/challenges/verify \ -H 'X-API-Key: $LIGHTNING_ENABLE_API_KEY' \ -H 'Content-Type: application/json' \ -d '{"macaroon":"AgEL...","preimage":"deadbeef..."}' { "valid": true, "resource": "/api/premium/weather", "merchantId": 42, "amountSats": 100, "paymentHash": "abc123" } ``` Once you see `valid: true`, serve the response. Note that this example did **not** include `resource` in the verify body, so the path caveat was reported back but not compared server-side — asserting that the returned `resource` matches the path the caller is requesting is your responsibility here. To get server-side enforcement instead, include the path in the verify request: ```bash -d '{"macaroon":"AgEL...","preimage":"deadbeef...","resource":"/api/premium/weather"}' ``` With `resource` supplied, a token bound to a different path returns `valid: false` — see [caveat enforcement rules](#token-reuse-within-the-validity-window) above. --- ## Language-specific quick references These are the minimum to call the producer API from each language. For richer ergonomics use the SDKs/middlewares. ### Node.js (without the SDK) ```js const response = await fetch("https://api.lightningenable.com/api/l402/challenges", { method: "POST", headers: { "X-API-Key": process.env.LIGHTNING_ENABLE_API_KEY, "Content-Type": "application/json", }, body: JSON.stringify({ resource: "/api/premium/weather", priceSats: 100, }), }); const challenge = await response.json(); ``` Prefer the [`l402-server`](https://www.npmjs.com/package/l402-server) SDK or [`l402-express`](https://www.npmjs.com/package/l402-express) middleware. ### .NET (without the SDK) ```csharp using var http = new HttpClient(); http.DefaultRequestHeaders.Add("X-API-Key", apiKey); var body = JsonContent.Create(new { resource = "/api/premium", priceSats = 100 }); var response = await http.PostAsync("https://api.lightningenable.com/api/l402/challenges", body); var challenge = await response.Content.ReadFromJsonAsync(); ``` Prefer [`L402Server`](https://www.nuget.org/packages/L402Server) or [`L402Server.AspNetCore`](https://www.nuget.org/packages/L402Server.AspNetCore). ### Python ```python import os import requests response = requests.post( "https://api.lightningenable.com/api/l402/challenges", headers={ "X-API-Key": os.environ["LIGHTNING_ENABLE_API_KEY"], "Content-Type": "application/json", }, json={"resource": "/api/premium/weather", "priceSats": 100}, ) challenge = response.json() ``` ### Go ```go body, _ := json.Marshal(map[string]any{ "resource": "/api/premium/weather", "priceSats": 100, }) req, _ := http.NewRequest("POST", "https://api.lightningenable.com/api/l402/challenges", bytes.NewReader(body)) req.Header.Set("X-API-Key", os.Getenv("LIGHTNING_ENABLE_API_KEY")) req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() var challenge map[string]any json.NewDecoder(resp.Body).Decode(&challenge) ``` --- ## Versioning The producer API is stable. New optional fields may be added to request/response bodies without a version bump; breaking changes ship behind a versioned path (`/api/v2/...`) with overlap. Subscribe to release notes at https://docs.lightningenable.com/release-notes. ## Rate limits Lightning Enable rate-limits per merchant API key. Limits are generous for typical traffic; if you're hitting them you'll see `429 Too Many Requests`. Contact support if you need higher limits. ## Support Open issues at the relevant SDK/middleware repo, or contact us at **support@lightningenable.com**. ## See also - [Sell With Your Agent](/getting-started/sell-with-your-agent) — an MCP agent driving every endpoint on this page end to end, from an empty account to a live, verified paid endpoint - [Verify ownership of your API](./verify-ownership) — prove you control a proxy's target so it can be listed publicly ============================================================================== # How producer pricing works Source: https://docs.lightningenable.com/products/agentic-commerce/producer-pricing ============================================================================== # How producer pricing works Producer pricing has four parts. This page describes their structure only. It doesn't state any figure, because every figure is configuration that can change: read the live values from the quote endpoints listed below. ## The four parts | Part | What it is | Who sets it | |---|---|---| | **Claim fee** | A small one-time payment when you claim a producer account. It covers your first period. | Lightning Enable | | **Renewal** | A payment per period, priced in dollars and converted to sats when you ask for it. The base price includes one active proxy. | Lightning Enable | | **Per-proxy increment** | An amount added to the renewal for each active proxy beyond the ones the base price includes. | Lightning Enable | | **Buyer price per call** | What a buyer pays for each call to your proxy. If you don't set one when you claim, a default applies. | You | The claim fee is there to keep out spam signups, not to earn revenue. The renewal and the per-proxy increment pay for the hosted service. The buyer price per call is your revenue: buyers pay it to your wallet. Lightning Enable does not hold funds; your wallet provider facilitates custody and settlement. ## Where each figure is read Read every figure from a quote at the moment you need it. A quote has no side effects and creates no invoice. | Figure | Where to read it | |---|---| | Claim fee | `GET /api/producer/claim`, in the `price` block (`sats` and `usdCents`). The `covers` block says how many days the claim covers. | | Renewal, as published with the claim | `GET /api/producer/claim`, in the `renewal` block: `baseUsdCents` and `baseSats`, its conversion at the live rate kept between the configured minimum and maximum, `includedProxies`, `periodDays`, and the renewal `endpoint`. | | Renewal, as you'd pay it now | `GET /api/merchant/subscription/renew`, in `priceSats` and `periodDays`, with the full calculation in `breakdown`. This is the amount a renewal challenge charges. If it differs from the claim quote's `renewal` block, this one is what you pay. | | Per-proxy increment | `GET /api/producer/claim`, in `renewal.perProxyUsdCents` and `renewal.perProxySats`, its conversion at the live rate. When no rate is available, the minimum applies and `perProxySats` is zero. | | Your current renewal and proxy count | `GET /api/producer/claim/status`, in `renew.quoteSats`, `renew.usdCents`, and `renew.activeProxies`. | | Buyer price per call | The price you set on the proxy. The claim quote's `limits` block gives the lowest and highest price a claimed proxy accepts (`minProxyPriceSats`, `maxProxyPriceSats`). | Prices that are pegged to dollars are converted to sats when the quote or the challenge is issued, so the sats figure moves with the exchange rate. If the exchange rate is unavailable, the renewal is priced at its configured sats minimum rather than refused, and the sats figure is always kept between a configured minimum and maximum. A renewal challenge you already hold is honored at the sats amount it was issued for, even if the price changes before you pay. The challenge still expires, and the usual eligibility rules still apply. The claim endpoint answers only where the claim flow is turned on. The renewal quote is described in the [Merchant Settings API](/api-reference/merchant-settings#renew-with-lightning-l402), and paying it is walked through in [Renew in sats](/getting-started/activate-with-lightning#renew-in-sats). ## How the renewal is calculated The renewal is priced per endpoint, and an endpoint here means an active proxy on your account. Inactive proxies and proxies on other accounts don't count. The calculation is: - The base price, which includes the number of proxies in `includedProxies`. - Plus the per-proxy increment for each active proxy beyond those. - Converted to sats at the current exchange rate, then kept within the configured minimum and maximum. The renewal quote's `breakdown` block shows each step for your account: `baseUsdCents`, `perProxyUsdCents`, `includedProxies`, `activeProxies`, `billableExtraProxies`, `usdCentsTotal`, the `btcUsd` rate used, the resulting `sats`, and `pricedAt`. To lower the renewal, deactivate the proxies you no longer need before you renew. Each payment adds one period (`periodDays`) to your paid-through date. You can prepay ahead, up to a limit; past it, the quote answers `eligible: false` with `reason: "PrepaidCapReached"` until the paid-through date comes closer. ## Signals before a period ends A producer account might have no email address, so Lightning Enable signals an upcoming lapse to your agent and to your buyers' agents in two ways: - **The `l402.subscription.expiring` webhook.** If your account has a callback URL, Lightning Enable sends this event about seven days and about one day before your paid-through date. It carries the paid-through date, the days left, the renew endpoint, and the current renewal quote in sats. Set the callback URL with [`PUT /api/merchant/webhook-url`](/api-reference/merchant-settings#update-webhook-url). The payload is described in the [Producer API Reference](/products/agentic-commerce/producer-api-reference#subscription-expiring-webhook). - **The proxy manifest.** The JSON manifest of each proxy you own carries `paid_through` and `renewal { endpoint, period_days }` in its `service` block, so a buyer's agent can see a lapse coming. The manifest carries no price; the live figure is always the quote. You can also read `paidThrough` from `GET /api/producer/claim/status` at any time. ## Figures are configuration Every figure on this page is configuration and can change without a docs update. Don't copy a figure from a quote into your code or your own docs. Read the quote each time, and set any spending limit in your agent from the quoted amount. ## What happens when a period lapses Nothing charges you automatically. When your paid-through date passes: - Authenticated calls answer `403` with `action_required: "renew_subscription"` and the renew endpoint. - A daily job moves the account to the [Free Producer Sandbox](/getting-started/activate-with-lightning), and the sandbox limits apply to new proxies and to challenge volume. Your existing proxies stay in place and keep serving buyers. - Nothing is deleted. Your account, API key, proxies, prices, and payment history stay in place, and the account stays active so it can reach the renew endpoint. - Paying the renewal restores the plan. For a lapsed account, the new period starts from your payment; a renewal paid before the period ends adds its period to your current paid-through date instead. Sats that buyers already paid went to your wallet when they paid, so a lapse never holds back money you earned. ============================================================================== # Proxy Configuration Source: https://docs.lightningenable.com/products/agentic-commerce/proxy-configuration ============================================================================== # Proxy Configuration The L402 Proxy allows you to monetize any API - yours or third-party - by creating a payment-gated reverse proxy. :::info No Infrastructure Required The L402 proxy is a fully hosted service. You configure it; Lightning Enable runs it. No Lightning node, no servers, no Docker. ::: ## Overview The proxy sits between clients and target APIs: ``` Client → L402 Proxy → Target API │ └─ Requires Lightning payment ``` Use cases: - **Monetize your own APIs** — in Proxy mode (no modification) or via [native middleware](./native-integration) (one-line drop-in) - **Resell third-party APIs** with a markup - **Create premium access** to public APIs - **Rate-limit expensive APIs** via micropayments ## Creating a Proxy ### Via Dashboard (Recommended) Navigate to [api.lightningenable.com/dashboard/proxies/create](https://api.lightningenable.com/dashboard/proxies/create). **Smart Setup** scans your API for an OpenAPI spec and imports its endpoints; **Skip auto-detect and configure manually** opens a 4-step wizard: 1. **Basic Info** — name and description 2. **Target** — the API you want to monetize 3. **Pricing** — satoshis per request + token validity 4. **Review** — confirm and create ![Create Proxy Wizard](/img/dashboard/proxy-create-step1.png) For a full visual walkthrough, see the [Dashboard Guide](/products/agentic-commerce/dashboard-guide). ### Via API ```bash curl -X POST https://api.lightningenable.com/api/proxy \ -H "X-API-Key: your-merchant-api-key" \ -H "Content-Type: application/json" \ -d '{ "name": "Premium Weather API", "targetBaseUrl": "https://api.weather.com/v1", "defaultPriceSats": 10, "description": "Weather data with Lightning payments" }' ``` Response (`201 Created`): ```json { "id": 12, "proxyId": "premium-weather-api-a1b2", "name": "Premium Weather API", "description": "Weather data with Lightning payments", "targetBaseUrl": "https://api.weather.com/v1", "defaultPriceSats": 10, "isActive": true, "createdAt": "2026-07-03T12:00:00Z", "requestCount": 0, "totalSatsEarned": 0, "endpointPricingCount": 0, "proxyUrl": "/l402/proxy/premium-weather-api-a1b2", "endpointPricings": [] } ``` The `proxyId` slug is generated from the proxy name. If the slug collides with an existing proxy, a 4-character hex suffix is appended (as in the example above); otherwise the clean slug is used as-is. On the Free Producer Sandbox you can create 1 proxy; a second returns `402 plan_proxy_limit`. Proxy IDs that start with `shopify` are reserved for the Shopify integration. A name whose slug would start with `shopify` (for example, `Shopify Orders`) returns `400 reserved_proxy_id`; choose a name that doesn't start with "Shopify". If you later change a proxy's `targetBaseUrl`, the proxy loses its ownership verification and is unlisted until you verify the new target. See [Verify ownership of your API](./verify-ownership.md#change-the-target-url). ### Proxy Endpoint Your proxy is now available at: ``` https://api.lightningenable.com/l402/proxy/{proxyId}/{path} ``` For example: ``` https://api.lightningenable.com/l402/proxy/premium-weather-api-a1b2/forecast?city=nyc ``` ## Configuration Options ### Configuration Reference Create (`POST /api/proxy`) accepts: | Field | Type | Required | Description | |-------|------|----------|-------------| | `name` | string | Yes | Display name for the proxy (1–100 chars). Also used to generate the `proxyId` slug. | | `targetBaseUrl` | string | Yes | Base URL of the target API. Must be `http`/`https` with a public domain name (see [SSRF Protection](#ssrf-protection)). | | `defaultPriceSats` | int | No | Default price per request in satoshis (1–1,000,000). Defaults to 10 if omitted. | | `description` | string | No | Proxy description (up to 500 chars). | | `tokenValiditySeconds` | int | No | How long a paid token stays usable (600–86,400). Defaults to 1 hour. | Update (`PUT /api/proxy/{proxyId}`) accepts the same fields — all optional — plus: | Field | Type | Required | Description | |-------|------|----------|-------------| | `isActive` | bool | No | Enable/disable the proxy. Disabled proxies return `404` to clients. | There are no per-proxy method filters, path allow/block lists, or timeout settings. The proxy forwards GET, POST, PUT, DELETE, PATCH, and HEAD on **all** paths under `/l402/proxy/{proxyId}/*` to the target API, with a fixed 30-second upstream timeout. `OPTIONS` is never forwarded or priced: the proxy answers it itself (a CORS preflight with the CORS headers, any other `OPTIONS` with `204 No Content` and an `Allow` header). If you need to restrict which endpoints are reachable, enforce that on your upstream API itself, or point `targetBaseUrl` at a narrower base path (e.g., `https://api.example.com/v1/public`). :::warning No path filtering Because there is no `allowedPaths`/`blockedPaths` setting, any path on your target API under the configured base URL is reachable through the proxy (after payment). Never point a proxy at an API surface that includes admin or internal endpoints you don't want exposed. ::: One limit does apply: the full L402 resource string `/l402/proxy/{proxyId}/{path}` can be at most **848 characters**. A longer request gets `414 resource_too_long` before any invoice is created — shorten the downstream path (or the proxy ID). ### Token Validity Each proxy has a token validity period: how long a paid L402 token remains usable after payment, from 10 minutes to 24 hours. Set it in the dashboard wizard (**Step 3: Pricing**) or with `tokenValiditySeconds`; if not set, tokens last 1 hour. ## Endpoint-Specific Pricing :::tip Dashboard Alternative You can manage endpoint-specific pricing visually in the [dashboard](/products/agentic-commerce/dashboard-guide#pricing-tab) — no API calls needed. ::: Set different prices for different endpoints: ```bash curl -X POST https://api.lightningenable.com/api/proxy/{proxyId}/pricing \ -H "X-API-Key: your-merchant-api-key" \ -H "Content-Type: application/json" \ -d '{ "pathPattern": "/forecast/7day", "priceSats": 50, "description": "7-day forecast (premium)" }' ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `pathPattern` | string | Yes | Exact path, a prefix ending in `/*`, or one `*` inside the path (`/series/*/latest`). Up to 200 chars, relative to the proxy root. | | `priceSats` | int | Yes | Price for matching requests (0–1,000,000). | | `priority` | int | No | Lower is checked first. Defaults to `0`. | | `description` | string | No | Up to 200 chars. | Change a rule with `PUT /api/proxy/{proxyId}/pricing/{pricingId}` (any of the fields above, plus `isActive`) and remove it with `DELETE /api/proxy/{proxyId}/pricing/{pricingId}`. ### Multiple Price Tiers ```bash # Add pricing for different endpoints curl -X POST .../pricing -d '{"pathPattern": "/current/*", "priceSats": 5}' curl -X POST .../pricing -d '{"pathPattern": "/forecast/1day", "priceSats": 10}' curl -X POST .../pricing -d '{"pathPattern": "/forecast/7day", "priceSats": 50}' curl -X POST .../pricing -d '{"pathPattern": "/historical/*", "priceSats": 100}' ``` ### Price Matching Priority Pricing rules are evaluated in ascending `priority` order (lower number = checked first; the default is `0`). The **first active rule whose `pathPattern` matches** the request path wins — there is no specificity ordering beyond that. If no rule matches, the proxy's `defaultPriceSats` applies. The public manifest uses the same resolution: each per-request, sats-priced endpoint's advertised price (`base_price_sats` in the JSON manifest, `x-payment.price_sats` in the OpenAPI document) is what these rules charge for that path, with any `{parameter}` segment matched the way a real request is. Changing a rule changes the advertised price too. Because a template path is resolved with a placeholder value, a rule for one specific value (an exact rule like `/series/GDP`) or a middle-wildcard rule that matches only some values isn't reflected in the advertised template price. USD-priced endpoints and non-per-request pricing models keep the values set on the endpoint. If you have overlapping patterns (e.g., `/forecast/7day` and `/forecast/*`), give the more specific rule a lower `priority` value so it is checked first: ```bash curl -X POST .../pricing -d '{"pathPattern": "/forecast/7day", "priceSats": 50, "priority": 0}' curl -X POST .../pricing -d '{"pathPattern": "/forecast/*", "priceSats": 10, "priority": 10}' ``` ## Using the Proxy ### Request Flow 1. Client requests proxy endpoint (no auth): ```bash curl https://api.lightningenable.com/l402/proxy/premium-weather-api-a1b2/forecast?city=nyc ``` 2. Proxy returns 402 with invoice: ```json { "error": "Payment Required", "message": "Pay the Lightning invoice to access this API", "proxy": { "id": "premium-weather-api-a1b2", "name": "Premium Weather API", "description": "Weather data with Lightning payments" }, "l402": { "macaroon": "AgEL...", "invoice": "lnbc100n1p...", "amount_sats": 10, "payment_hash": "abc123...", "expires_at": "2026-07-03T12:10:00.0000000Z" }, "instructions": { "step1": "Pay the Lightning invoice using any Lightning wallet", "step2": "Copy the preimage (proof of payment) from your wallet", "step3": "Include in request: Authorization: L402 : (or Authorization: Payment method=\"lightning\", preimage=\"\")" } } ``` 3. Client pays invoice, gets preimage 4. Client retries with L402 credential: ```bash curl https://api.lightningenable.com/l402/proxy/premium-weather-api-a1b2/forecast?city=nyc \ -H "Authorization: L402 AgEL...:abc123..." ``` 5. Proxy forwards request to target, returns response: ```json { "city": "New York", "forecast": [...] } ``` ## Proxy Management ### List Your Proxies ```bash curl https://api.lightningenable.com/api/proxy \ -H "X-API-Key: your-merchant-api-key" ``` ### Get Proxy Details ```bash curl https://api.lightningenable.com/api/proxy/{proxyId} \ -H "X-API-Key: your-merchant-api-key" ``` ### Update Proxy ```bash curl -X PUT https://api.lightningenable.com/api/proxy/{proxyId} \ -H "X-API-Key: your-merchant-api-key" \ -d '{"defaultPriceSats": 20}' ``` ### Delete Proxy ```bash curl -X DELETE https://api.lightningenable.com/api/proxy/{proxyId} \ -H "X-API-Key: your-merchant-api-key" ``` ## Target API Authentication Lightning Enable **does not store or inject upstream API credentials**. The proxy forwards client headers transparently and strips the incoming `Authorization` header (it consumes that one for the L402 payment check). Cookies never cross the proxy: the proxy drops the incoming `Cookie` header and your API's `Set-Cookie` response headers, so your API can't rely on a cookie-based session through the proxy. There is no `authHeader`/`authValue` configuration. If the API you are proxying requires its own authentication (an API key, bearer token, signed requests), you have two options: 1. **Merchant-side auth shim** — put a thin endpoint you control between the proxy and the credentialed API: the proxy targets your endpoint, and your endpoint attaches the credential when calling the upstream. See [Handling APIs that require authentication](/products/agentic-commerce/proxy-setup-walkthrough#handling-apis-that-require-authentication) in the setup walkthrough for the full pattern. 2. **Native integration** — skip the proxy and add L402 payment gating directly inside your own API with the [native middleware](./native-integration). Your code keeps full control of upstream credentials; they never leave your infrastructure. ## Analytics :::tip Dashboard Alternative The same lifetime totals are visible on the proxy detail page's [Overview tab](/products/agentic-commerce/dashboard-guide#overview-tab) in the dashboard. Per-day charts and per-path breakdowns are on the roadmap but not available yet. ::: ### Get Proxy Analytics ```bash curl https://api.lightningenable.com/api/proxy/{proxyId}/analytics \ -H "X-API-Key: your-merchant-api-key" ``` Response — lifetime totals for the proxy (there is no time-period filtering or per-path breakdown): ```json { "proxyId": "premium-weather-api-a1b2", "name": "Premium Weather API", "totalRequests": 5234, "totalSatsEarned": 52340, "averageRevenuePerRequest": 10.00, "createdAt": "2026-05-01T09:00:00Z", "lastUpdated": "2026-07-03T11:58:21Z" } ``` ## Testing Proxy ### Test Target Reachability ```bash curl -X POST https://api.lightningenable.com/api/proxy/{proxyId}/test \ -H "X-API-Key: your-merchant-api-key" ``` Response: ```json { "success": true, "targetUrl": "https://api.weather.com/v1", "statusCode": 200, "statusDescription": "OK", "responseTimeMs": 234, "message": "Target API is reachable" } ``` On failure, `success` is `false` and `message` explains why (e.g., `"Request timed out after 10 seconds"` or `"Connection failed: ..."`). The test request uses a 10-second timeout and runs the same SSRF checks as live proxy traffic. ### Test with Specific Path Pass the path as a **query parameter** (there is no JSON request body): ```bash curl -X POST "https://api.lightningenable.com/api/proxy/{proxyId}/test?path=/forecast" \ -H "X-API-Key: your-merchant-api-key" ``` ## Example Configurations ### Public Weather API Proxy An API that needs no upstream credentials — the simplest case: ```json { "name": "Weather Data", "description": "Public weather data with Lightning payments", "targetBaseUrl": "https://api.open-meteo.com/v1", "defaultPriceSats": 5 } ``` ### Your Own API with Tiered Pricing Point the proxy at your API and layer endpoint-specific prices on top of the default: ```json { "name": "Market Data", "description": "Market data API, pay-per-request", "targetBaseUrl": "https://api.your-domain.com/v2", "defaultPriceSats": 10 } ``` Endpoint pricing (added via `POST /api/proxy/{proxyId}/pricing`): ```json [ { "pathPattern": "/quotes/realtime/*", "priceSats": 50, "priority": 0 }, { "pathPattern": "/quotes/*", "priceSats": 10, "priority": 10 } ] ``` ### Credentialed Upstream (via Auth Shim) For an upstream that requires its own API key (e.g., an AI model provider), target a thin endpoint **you** host that attaches the credential — the proxy itself never holds it: ```json { "name": "AI Completions", "description": "Pay-per-request AI completions", "targetBaseUrl": "https://shim.your-domain.com/ai", "defaultPriceSats": 500 } ``` See [Target API Authentication](#target-api-authentication) above for why this pattern is required and what the shim looks like. ## Security The L402 proxy includes several built-in protections to prevent abuse and ensure safe operation. These protections apply automatically to all proxies. ### Request & Response Size Limits The proxy enforces size limits on both inbound requests and upstream responses to prevent memory exhaustion and abuse. | Limit | Default | Config Key | HTTP Status on Violation | |-------|---------|------------|--------------------------| | Request body | 1 MB (1,048,576 bytes) | `L402:MaxProxyRequestBodyBytes` | **413 Payload Too Large** | | Response body | 10 MB (10,485,760 bytes) | `L402:MaxProxyResponseBodyBytes` | **502 Bad Gateway** | **Request body limit** -- When a client sends a POST, PUT, or PATCH request through the proxy, the body is checked against the configured maximum. If the `Content-Length` header is present and exceeds the limit, the request is rejected immediately. If the header is absent, the body is read incrementally and rejected as soon as it exceeds the limit. **Response body limit** -- When the upstream target API returns a response, its size is checked the same way: first via `Content-Length` header for an early rejection, then by streaming and monitoring the total bytes read. If the response exceeds the limit, the proxy returns a 502 to the client instead of the oversized response. These limits are set at the Lightning Enable service level (`L402:MaxProxyRequestBodyBytes` / `L402:MaxProxyResponseBodyBytes`) — they are not per-proxy merchant settings. If your use case needs larger payloads, contact support. **Example error response (413):** ```json { "error": "Payload Too Large", "message": "Request body size (2,500,000 bytes) exceeds the maximum allowed size (1,048,576 bytes)", "proxy_id": "premium-weather-api-a1b2" } ``` **Example error response (502 for oversized upstream response):** ```json { "error": "Bad Gateway", "message": "Response from target API (15,000,000 bytes) exceeds the maximum allowed size (10,485,760 bytes)", "proxy_id": "premium-weather-api-a1b2" } ``` ### SSRF Protection Server-Side Request Forgery (SSRF) protections prevent proxies from being used to access internal infrastructure. Validation happens at **two stages**: when configuring a proxy and at runtime when forwarding each request. #### Configuration-Time Validation When you create or update a proxy, the `targetBaseUrl` is validated against these rules: | Rule | Details | |------|---------| | **Scheme** | Only `http` and `https` are allowed. Other schemes (e.g., `file://`, `ftp://`) are rejected. | | **Hostname** | Raw IP addresses are not allowed; a domain name is required. | | **Localhost rejection** | `localhost`, `localhost.localdomain`, `ip6-localhost`, and `ip6-loopback` are blocked. | | **Internal domain suffixes** | Hostnames ending in `.local`, `.internal`, `.localhost`, or `.svc.cluster.local` are blocked. | | **Port restrictions** | Only standard HTTP ports (80, 443) are allowed. Non-standard ports are rejected. | | **DNS resolution check** | If the hostname resolves at configuration time, all resolved IPs are checked for private ranges. If DNS resolution fails (e.g., the domain is not yet set up), the proxy is allowed but will be checked again at runtime. | #### Runtime DNS Rebinding Prevention Even if a domain passes configuration-time validation, it is checked again **on every proxied request**. Before connecting to the target API, the proxy resolves the hostname and verifies that none of the resolved IP addresses fall into private or reserved ranges. This prevents DNS rebinding attacks where an attacker changes a domain's DNS records to point to internal IPs after the proxy is configured. **Blocked IP ranges:** | Range | Description | |-------|-------------| | `127.0.0.0/8` | IPv4 loopback | | `10.0.0.0/8` | RFC 1918 private | | `172.16.0.0/12` | RFC 1918 private | | `192.168.0.0/16` | RFC 1918 private | | `169.254.0.0/16` | IPv4 link-local | | `0.0.0.0/8` | Current network | | `::1` | IPv6 loopback | | `fc00::/7` | IPv6 unique local | | `fe80::/10` | IPv6 link-local | | IPv4-mapped IPv6 | e.g., `::ffff:127.0.0.1` (mapped to IPv4 and checked) | If a blocked IP is detected at runtime, the proxy returns a **502 Bad Gateway** response: ```json { "error": "Bad Gateway", "message": "The target API address is not allowed", "proxy_id": "premium-weather-api-a1b2" } ``` :::tip These protections are fully automatic. You do not need to configure anything -- they are always active for all proxies. ::: ## Error Handling ### Proxy Errors | Status | Body `error` | Cause | Solution | |--------|--------------|-------|----------| | 404 | `Proxy not found` | Unknown **or disabled** proxy ID | Check the proxy ID; re-enable the proxy if you disabled it | | 404 | `Proxy unavailable` | The merchant account behind the proxy is inactive | Reactivate the merchant account | | 404 | `Ambiguous proxy ID` | An unsuffixed alias matches more than one proxy | Use the full proxy ID including its hex suffix | | 502 | `Bad Gateway` | Target API unreachable / connection failed | Check the target URL and target API health | | 504 | `Gateway Timeout` | Target API didn't respond within 30 seconds | Speed up the upstream endpoint (the timeout is fixed) | | 413 | `Payload Too Large` | Request body exceeds size limit | Reduce request body or contact provider about limits | | 414 | `resource_too_long` | The full `/l402/proxy/{proxyId}/{path}` resource exceeds 848 characters | Shorten the downstream path or the proxy ID; no invoice is created for the refused request | | 502 | `Bad Gateway` (size) | Upstream response exceeds size limit | Contact provider about response size limits | | 502 | `Bad Gateway` (SSRF) | Target resolves to private IP | Use a public domain name for your target API | | 503 | `payment_provider_not_configured` | The proxy owner has no payment provider set up, so no invoice could be created | Owner: add a provider under **Dashboard → Settings → Payment Provider** | | 503 | `payment_provider_busy` | The owner's NWC wallet has too many requests in flight | Retry after `Retry-After` | | 503 | `challenge_persist_failed` | The challenge couldn't be recorded, so no invoice was issued | Retry | Error bodies include a `message` field with details and (for gateway errors) a `proxy_id` field. ### Payment / Credential Errors The proxy **never returns 401 or 403**. Any missing, malformed, expired, or otherwise invalid L402 credential results in a **fresh `402 Payment Required` challenge** — a new invoice and macaroon — with the failure reason in the `X-L402-Error` response header and in the body's `message` field. | Status | Meaning | What the client should do | |--------|---------|---------------------------| | 402 (no `X-L402-Error`) | First request — payment required | Pay the invoice, retry with `Authorization: L402 :` | | 402 + `X-L402-Error` | The presented credential failed verification (bad format, wrong preimage, expired token, wrong path/merchant/price) | Read the header for the reason; pay the **new** invoice from this response and retry | Clients that branch on 401/403 will never hit those branches — treat every 402 as a (re-)challenge. ## Best Practices ### Security - Keep upstream API credentials on your own infrastructure (auth shim or native integration) — the proxy does not store them - Scope `targetBaseUrl` to the narrowest base path that serves your paying clients; every path under it is reachable - Never expose an API surface containing admin or internal endpoints through a proxy - Monitor usage for abuse - Use HTTPS target URLs whenever possible ### Pricing - Research target API costs - Add reasonable markup (20-50%) - Consider volume discounts - Price based on value, not cost ### Reliability - Keep upstream responses under the fixed 30-second proxy timeout - Handle target API errors gracefully - Monitor target API availability - Have fallback targets if possible ## Next Steps - [Dashboard Guide](/products/agentic-commerce/dashboard-guide) - Visual proxy management walkthrough - [API Reference](/api-reference/l402) - Complete L402 API docs - [How It Works](/products/agentic-commerce/how-it-works) - Technical details - [FAQ](/faq) - Common questions ============================================================================== # Setting Up Your Proxy (Full Walkthrough) Source: https://docs.lightningenable.com/products/agentic-commerce/proxy-setup-walkthrough ============================================================================== # Setting Up Your Proxy This guide walks you through every step required to take an existing API and make it discoverable and payable by AI agents using the L402 protocol. You do not need to change any code in your API. **Who this is for:** API providers who want AI agents to find their API, understand what it does, and pay per request in Bitcoin over Lightning. If you are an agent developer looking to consume an existing L402 API, see [MCP Quickstart](/products/agentic-commerce/mcp-quickstart) instead. **What you will have at the end:** A live proxy URL that enforces per-request Lightning payments, a public manifest that AI agents can read to discover your endpoints and pricing, and an optional listing in the public L402 registry. --- ## Table of Contents 1. [Prerequisites](#prerequisites) 2. [Step 1 — Create a proxy](#step-1--create-a-proxy) 3. [Step 2 — Set a default price](#step-2--set-a-default-price) 4. [Step 3 — Configure the manifest](#step-3--configure-the-manifest) 5. [Step 4 — Add your endpoints](#step-4--add-your-endpoints) 6. [Step 5 — Review pricing per endpoint](#step-5--review-pricing-per-endpoint) 7. [Step 6 — Publish and share](#step-6--publish-and-share) 8. [Troubleshooting](#troubleshooting) --- ## Prerequisites - A Lightning Enable account. The **Free Producer Sandbox** includes 1 proxy (3 endpoints, 200 challenges a month, 1,000 sats max per challenge); **Agentic Commerce** ($49/mo, 30-day free trial) removes the caps; **Agentic Commerce — Business** is [contact us](mailto:support@lightningenable.com). - A payment provider — Strike (recommended), OpenNode, or an NWC wallet — saved under **Dashboard → Settings → Payment Provider**. - An API you own or have permission to proxy, on a public domain name and a standard port (80 or 443). :::tip Your API needs no changes The proxy wraps your API from the outside. Your API never knows that payments are happening. It receives normal HTTP requests and returns normal HTTP responses. ::: --- ## Step 1 — Create a proxy Open the dashboard at [api.lightningenable.com/dashboard](https://api.lightningenable.com/dashboard) and click **Create Proxy**. The page opens in **Smart Setup**: enter your API URL and click **Scan my API's OpenAPI spec**. If a spec is found, Lightning Enable imports every endpoint and pre-fills the manifest details from the spec; you set a name and default price, then click **Create proxy + import endpoints** (you can skip Steps 3 and 4 below except to review). If your API has no spec, click **Skip auto-detect and configure manually** for the 4-step wizard (Basic Info, Target, Pricing, Review), which the rest of this step describes. ### Name Pick a name your customers and agents will recognize. This appears in the proxy list and in the public manifest. **Good example:** `OpenWeather Current Conditions` **Avoid:** `proxy1`, `test`, `my api` ### Proxy ID Lightning Enable generates a Proxy ID from your name: lower-cased, with spaces as hyphens, plus a 4-character hex suffix only if that slug is already taken. `OpenWeather Current Conditions` becomes `openweather-current-conditions`. The Proxy ID becomes part of the URL where your proxy lives: ``` https://api.lightningenable.com/l402/proxy/openweather-current-conditions/ ``` AI agents and clients send their requests to this URL. The proxy forwards them to your actual API. You cannot change the Proxy ID after creation. ### Target URL This is the base URL of the real API you are proxying. Every request that arrives at your proxy URL is forwarded to this address. | Your proxy URL | Gets forwarded to | |---|---| | `/l402/proxy/openweather-current-conditions/current?city=miami` | `https://api.openweathermap.org/data/2.5/current?city=miami` | The URL must be `http://` or `https://` on a public domain name and a standard port (80 or 443). Raw IP addresses, internal hostnames, and targets resolving to private IP addresses are rejected, both when you create the proxy (dashboard or API) and again on every forwarded request. See [SSRF Protection](/products/agentic-commerce/proxy-configuration#ssrf-protection). ### Handling APIs that require authentication Lightning Enable **does not** store or inject upstream API credentials. The proxy forwards client headers transparently and strips the incoming `Authorization` header (LE consumes that one for the L402 payment check). It also drops cookies in both directions (the incoming `Cookie` header and your upstream's `Set-Cookie`). If the API you are proxying requires its own auth — an API key, bearer token, signed request, mTLS — that authentication has to happen between your infrastructure and your upstream, not through us. **Recommended pattern: a thin auth-injecting front in your own infrastructure.** Stand up a small reverse proxy or edge worker on a subdomain you control (for example, `monetized-api.your-domain.com`) that: 1. Accepts unauthenticated incoming requests from Lightning Enable 2. Adds your upstream API key / bearer / signature 3. Forwards to your real API Then set your Lightning Enable proxy's **Target URL** to that subdomain instead of your raw API. Your credentials stay in your environment, you can rotate them without touching Lightning Enable, and LE never sees them. **Example — nginx:** ```nginx location / { proxy_pass https://api.your-upstream.com; proxy_set_header Authorization "Bearer YOUR_UPSTREAM_KEY"; proxy_set_header Host api.your-upstream.com; } ``` Adapt the same pattern to whatever edge-proxy / API-gateway / serverless function your stack already uses — the only requirement is that LE-bound traffic gets an `Authorization` header attached before it reaches your real upstream. If your API has no auth (it's already public and you want to add Lightning monetization in front of it), skip this step — just set Target URL to your API directly. :::tip Deeper integration available For commercial APIs where running a separate auth-injecting proxy isn't a fit, [**Native L402 integration**](./native-integration) is available. Your API validates L402 tokens directly using Lightning Enable's hosted producer endpoints, and Lightning Enable acts as a payment broker that never sees your traffic. Middleware packages exist for [Express (Node)](./native-integration-express) and [ASP.NET Core (.NET)](./native-integration-aspnet). ::: --- ## Step 2 — Set a default price After creating the proxy you land on the proxy detail page. Open the **Pricing** tab. The **Default Fallback Price** is the number of satoshis charged for any request that does not match a per-endpoint rule. Think of it as the "everything else" price. If you charge the same amount for every endpoint, set this once and you are done. If you want different prices for different endpoints, set a reasonable base price here and then configure per-endpoint overrides in Step 5. **How to choose a starting price:** The wizard shows an approximate USD equivalent next to the number you enter. Use this table as a rough starting point: | Use case | Suggested starting price | |---|---| | Public data (weather, sports scores) | 10–50 sats | | Enrichment APIs (address validation, geocoding) | 50–200 sats | | AI inference (summarization, classification) | 200–1000 sats | | Proprietary data feeds | Set based on your commercial price | Enter the number in the **Price per Request (sats)** field under **Default price (fallback)** and click **Save**. --- ## Step 3 — Configure the manifest The manifest is a structured JSON file that lives at a public URL on your proxy. AI agents read it to learn what your API does, what it costs, and how to pay. Without a manifest, your proxy is invisible to agents — they have no way to discover it or know what it costs. With a manifest, your API can appear in the public L402 registry, agents can discover your endpoints automatically, and they can pay per call and use your API autonomously. ### Make this API visible to agents Turn on the **Make this API visible to agents** switch. This publishes the manifest file at: ``` https://api.lightningenable.com/l402/proxy/{proxyId}/.well-known/l402-manifest.json ``` Any agent that knows your proxy URL can fetch this file. The proxy also adds a `Link` header to every 402 response pointing to this file, so agents that encounter your proxy for the first time can auto-discover the manifest without knowing the URL in advance. ### Service Description One or two plain-language sentences describing what your API does. This text appears in: - Registry search results (when agents or developers search the public registry by keyword) - The top of the manifest JSON (agents read this to decide if your API is relevant to their current task) Write it for an AI agent, not a marketing audience. Concrete is better than superlative. **Good example:** `Returns current weather conditions and 7-day forecasts for any US city or zip code. Data refreshes every 15 minutes from NWS and NOAA sources.` **Avoid:** `The best weather API — fast, reliable, and affordable!` ### Contact Email A public email address for integration questions. Visible in the manifest. Human developers who want to use your API may reach out here. This field is optional. ### Documentation URL A link to your full API documentation — the human-readable docs you already have. AI agents may follow this URL to get endpoint-level detail beyond what the manifest captures. Optional but recommended if you have docs. ### Terms of Service URL A link to your terms of service or usage policy. Some agents check for a ToS URL before paying and require acceptance before they proceed. Optional. ### Save Settings Click **Save Settings** after filling in any of these fields. The manifest file updates immediately. --- ## Step 4 — Add your endpoints The endpoints table tells agents exactly which paths exist, what each one does, and what it costs. Without endpoints listed, the manifest advertises your default price but gives agents no detail about individual operations. You have two options for populating this table. ### Option A — Scan API (recommended if your API has an OpenAPI spec) If your API serves an OpenAPI (Swagger) specification, the scanner can read it and import all your endpoints automatically. **What Scan API does:** 1. Fetches your API's OpenAPI spec (either from a URL you provide, or by probing standard locations on your target URL). 2. Parses every endpoint: path, HTTP method, summary, description, request schema, response schema. 3. Imports them into the manifest endpoint table with your default price applied to each one. **Standard locations the scanner probes automatically:** When you leave **OpenAPI / Swagger spec URL** blank, the scanner tries these paths on your target URL: - `/openapi.json` - `/swagger/v1/swagger.json` - `/swagger.json` - `/api-docs` - `/v3/api-docs` - `/.well-known/openapi.json` If your spec is at one of these locations, click **Scan API** with the field empty. If the scan succeeds, you will see a confirmation message listing how many endpoints were imported. **If auto-scan fails:** The most common reason is that your spec is served at a non-standard URL. In that case, paste the full URL to your spec in the **OpenAPI / Swagger spec URL** field and click **Scan API** again. ``` Example: https://api.example.com/v2/docs/swagger.json ``` If the scan still fails, your API may not publish an OpenAPI spec. Use Option B instead. **What happens after a successful scan:** Discovered endpoints are added to the table with: - Your default fallback price set on each one - L402 payment enabled - The endpoint visible in the public manifest You can adjust any of these settings per-endpoint after import. **Scans are idempotent** — running Scan API a second time only imports endpoints not already in the table. Existing entries are not overwritten. ### Option B — Add Manual Click **Add Manual** to open a form where you enter a single endpoint by hand. Use this when: - Your API does not have an OpenAPI spec - You only want to expose a subset of endpoints - You want to add endpoints with custom descriptions that the spec does not include Fields to fill in: - **Path** — the URL path, starting with `/` (e.g., `/v1/forecast`) - **HTTP Method** — GET, POST, PUT, PATCH, or DELETE - **Summary** — one sentence describing what this endpoint does (shown in the manifest) - **Description** — optional longer explanation (agents may read this for context) - **Base Price (sats)** — sats per request for this specific endpoint The dialog also exposes optional fields you can leave at their defaults: - **Pricing Model** — Per-request (default), Free, or other models for unusual pricing - **Base Price (USD cents)** — set this if you want to price in USD; the manifest converts to sats live at the current rate - **Visible in Manifest** — toggle off to hide an internal endpoint - **L402 Required** — toggle off if this endpoint should be free / not gated - **Rate Limits** — per-minute and per-day caps that agents are expected to respect --- ## Step 5 — Review pricing per endpoint After populating the endpoint table, review the **Price** column on each row. Each row shows the price saved for that endpoint. The public manifest (JSON, Markdown, and OpenAPI) advertises what the proxy **charges** for the path — the first matching pricing rule, else the default fallback price — so an agent that reads your manifest is quoted the amount it will pay. (A template path such as `/series/{seriesId}` is resolved with a placeholder value, so a rule for one specific value, like `/series/GDP`, isn't reflected in its advertised price.) When the saved price and the charge differ, the row shows the Sync chip described below. ### Default vs per-endpoint prices There are two layers of pricing: **Default Fallback Price (top of the Pricing tab):** Charged when a request arrives at your proxy but does not match any per-endpoint rule. This is the safety net for paths you have not configured individually. **Per-endpoint prices (in the table):** The price saved for a specific endpoint. It becomes the charge only through a pricing rule. The Sync chip below and the edit dialog write one, and so do endpoints added through the API (`POST /api/proxy/{proxyId}/manifest/endpoints`, which the MCP `add_endpoint` action calls) — but only when the rule would govern this path and change the price of no other endpoint. Otherwise nothing is written and you see a note explaining why. Whatever is saved, the manifest advertises the price the proxy charges. A per-endpoint price with no rule behind it is not advertised; the chip shows you where that has happened. ### Editing per-endpoint pricing Click the **edit** icon on any row to change the price, description, summary, or visibility for that endpoint. | Field | Effect | |---|---| | Price (sats) | The saved price for this endpoint; saving a new price tries to make it the charge (and therefore the advertised price) | | In manifest | Whether this endpoint appears in the public manifest | | Requires payment | Whether payment is required for this endpoint | The **Test it** button on each row mints a live 402 for that endpoint and shows exactly what an agent receives: the invoice, the decoded macaroon caveats, and a curl command to copy. ### The Sync chip When a sats-priced endpoint's saved price differs from what the proxy actually charges for that path, a yellow **Price mismatch — fix** chip appears on that row. For example, a saved price of 100 sats with a charge of 50 sats means calls to this path cost 50 sats, and the public manifest advertises 50 sats — what callers pay. The 100 sats you saved isn't being charged anywhere. **What causes this:** The most common cause is that a wildcard pricing rule (like `/v1/*` at 50 sats) applies to this path at runtime, but the endpoint was later edited in the table with a different price. **What Sync does:** Clicking the chip and confirming writes a pricing rule at the saved price — an exact-path rule at the highest priority for a literal path, or a single-wildcard rule (`/series/*`) for a path with one `{parameter}`. The wildcard rule you already had remains and still covers other paths, but this path is now charged, and advertised, at the saved price. Sync writes nothing, and tells you why, when the rule would also change the price of another endpoint in the table (another HTTP method on the same path, or a path the wildcard also matches), when an existing rule takes precedence, or when the path can't be expressed as one rule. If you'd rather keep the current charge, edit the row's price to match it instead. After syncing, the chip disappears from that row. **When the chip does NOT appear (even if the numbers differ):** - **Free endpoints** (`Pricing Model = Free`) — they have no sats price to mirror, so Sync would have nothing meaningful to write - **USD-priced endpoints** (`Base Price (USD cents)` set) — the manifest converts USD to sats live at request time using the current BTC rate, so the runtime sats number is *expected* to drift. Sync is intentionally skipped here - **Endpoints with `Base Price (sats) = 0`** — same reason as Free; nothing positive to mirror - **Config-driven proxies** — runtime pricing lives in App Service config, not the database, so the dashboard can't compute a runtime number to compare against For these cases, if you want the runtime charge to match the saved price you'll need to address it through the appropriate channel: switch the endpoint to a sats-priced Per Request model and re-save, or update the App Service config directly for a config-driven proxy. --- ## Step 6 — Publish and share ### List in Public Registry Turn on the **List in Public Registry** switch to include your proxy in the public L402 API registry at: ``` https://api.lightningenable.com/api/manifests/registry ``` The registry is where AI agents and developers search for available L402 APIs by keyword or category. Once listed, your API appears in results matching your name, description, and service description. Requirements: - **Make this API visible to agents** must be on - **Service Description** is strongly recommended — it is the text that appears in search results and determines whether agents consider your API relevant Turning the switch on runs an automated check of the proxy's L402 configuration. If the check fails, the proxy is not listed and the dashboard shows why. ### The three URLs in Preview & Share The **Preview & Share** section at the bottom of the Pricing tab shows three URLs. **JSON URL** — the machine-readable manifest: ``` /l402/proxy/{proxyId}/.well-known/l402-manifest.json ``` This is what AI agents fetch. Share it with developers who want to write code that uses your API. Open it in a browser to verify everything looks right before announcing your API. **Markdown URL** — a human-readable preview of the same information at `/api/proxy/{proxyId}/manifest.md`. This endpoint is **authenticated** — it requires your merchant API key and is intended for previewing or exporting the manifest as Markdown, not for public sharing. To share the manifest externally, give people the JSON URL above (which is public) or copy the rendered Markdown into your own README / docs. **Registry URL:** ``` https://api.lightningenable.com/api/manifests/registry ``` This is the registry itself, not a URL specific to your proxy. Share it when pointing someone to the broader ecosystem of L402 APIs — for example, in a blog post or announcement. Agents use this URL to search for available APIs by keyword. ### OpenAPI document Every proxy with **Make this API visible to agents** turned on also serves a standard OpenAPI 3.1 document at: ``` /l402/proxy/{proxyId}/openapi.json ``` (the equivalent `.well-known/openapi.json` path under the same proxy prefix serves the identical document). This is public and unauthenticated — no merchant API key required, same visibility rule as the JSON manifest: it 404s until the manifest switch is on. Use it when the agent or tool you're pointing at your API already knows how to parse OpenAPI and doesn't need to learn the L402 manifest's custom schema. The document has one path item per visible, active manifest endpoint, and every operation carries: - A vendor `x-payment` extension with the price in sats (and USD, for USD-priced endpoints), the payment protocols currently advertised (`L402` and, when enabled, `MPP-draft00`), the token validity window, and the auth header format. - A `402` response object pointing at `components.responses.PaymentRequired`, which documents the `WWW-Authenticate` header carrying the L402 challenge. The document's `servers[0].url` is your proxy's live base URL (`/l402/proxy/{proxyId}`), and its root-level `x-payment` block carries your proxy's default price. Prices in the OpenAPI document and the JSON manifest are computed by the exact same pricing logic — they can never disagree. The JSON manifest's `service` block also links to this document via an `openapi_url` field, and the registry listing for your proxy includes an `openApiUrl` field plus an `x-payment` summary, so an agent that starts at either surface can find the OpenAPI document without a separate lookup. --- ## Troubleshooting ### "Could not find an OpenAPI/Swagger specification at any well-known location" This message appears when Scan API probes all standard locations and finds nothing. It does not mean your API is broken — it means your API does not expose an OpenAPI spec at a standard path, or the spec is protected behind authentication. **Try this:** 1. Check your API docs for a link to your Swagger or OpenAPI spec. 2. Paste the full URL to your spec in the **OpenAPI / Swagger spec URL** field and click **Scan API** again. 3. If your spec requires authentication, your API provider will need to either expose a public copy or you will need to add endpoints manually using **Add Manual**. Common non-standard spec paths: - `https://your-api.com/docs/openapi.yaml` - `https://your-api.com/api/swagger` - `https://your-api.com/specification.json` ### "Pricing mismatch detected — action recommended" This warning appears when the proxy has any active pricing rule that uses a wildcard pattern (a `PathPattern` containing `*`, like `/v1/*`). It does not by itself mean anything is wrong: it means that *if* an endpoint's saved price differs from what the wildcard charges for that path, the row shows a **Price mismatch — fix** chip, and fixing it writes a more specific exact-path rule that wins over the wildcard. **If you see yellow Sync chips on rows:** click Sync on each one to write an exact-path rule for that endpoint at its saved price. The wildcard remains and still applies to paths the table doesn't cover. After every sats-priced row has either matched the wildcard or been explicitly synced, the chips disappear (and the warning becomes informational only). **If you see the warning but no chips:** your manifest endpoints either already match the wildcard, are Free/USD-priced (where the chip is intentionally suppressed), or there are no entries to compare against. No action needed. **If you intended the wildcard price to apply to everything:** update the per-endpoint prices in the table to match the wildcard price, then sync each sats-priced row. ### The manifest shows no endpoints If you enabled the manifest but the endpoint table is empty, the JSON manifest will have an empty `endpoints` array. Agents can still discover your proxy and pay for requests using the default price, but they will not know what individual paths exist. Populate the endpoint table using Scan API or Add Manual. Only endpoints with **In manifest** turned on appear in the manifest. ### An endpoint I added does not appear in the manifest Check that: - The **In manifest** toggle on that row is on - **Make this API visible to agents** is turned on in the Pricing tab - You clicked **Save Settings** after enabling the manifest ### My proxy is active but agents get a 404 Verify that the Target URL in the Overview tab is correct and reachable. A 404 from the proxy itself means the proxy could not find your API at that URL. A 404 from your API means the path the agent requested does not exist on your API. --- ## Next Steps - [API Monetization](/products/agentic-commerce/api-monetization) — pricing strategies and markup guidance - [Proxy Configuration Reference](/products/agentic-commerce/proxy-configuration) — complete technical reference including security limits and error codes - [How L402 Works](/products/agentic-commerce/how-it-works) — the full payment protocol flow - [AI Agent Integration](/products/agentic-commerce/ai-agent-integration) — how agents consume your proxy ============================================================================== # Verify Ownership of Your API Source: https://docs.lightningenable.com/products/agentic-commerce/verify-ownership ============================================================================== # Verify ownership of your API A Lightning Enable proxy sits in front of an API (its *target*) and charges a small Lightning payment for each call. It uses L402, an HTTP payment protocol: a caller without a credential gets a *payment challenge* (an HTTP `402` response carrying a Lightning invoice), pays the invoice, and retries with proof of payment. Anyone with a Lightning Enable account can point a proxy at any public URL, so before a proxy appears in public listings, Lightning Enable asks the proxy's owner to prove that they control the target. This page shows you how to do that. You publish a token that only you received, in a place only the target's owner can write to, and then ask Lightning Enable to look for it. That place is one of three: a small JSON file on the target's host, a DNS TXT record, or a response header from the API itself. ## What verification gives you Verification is a tier, not a gate. An unverified proxy works exactly as it did before: callers can reach it by its direct URL, pay, and get answers. Verification adds three things: - **Public listing.** Only a verified proxy can be listed in the public L402 registry (the directory of paid APIs that agents search to find services), in agent discovery (`/api/agents/capabilities`, the same list served to agents), and on Nostr (a public relay network where Lightning Enable also announces listed APIs). If you try to list an unverified proxy, `PUT /api/proxy/{proxyId}/manifest/settings` with `"manifestPubliclyListed": true` answers `422` with `"error": "ownership_verification_required"` and a `verify_endpoint` to call first. - **Higher challenge caps for claim accounts.** If you created your account with the one-payment claim (`POST /api/producer/claim`, which creates a Lightning Enable account and its first proxy with a single Lightning payment), any unverified proxy of that account runs under a per-minute and per-month limit on payment challenges, including proxies you add later through the API or the dashboard. Past the limit, callers get `429` with `"error": "unverified_proxy_rate_limited"` and a `Retry-After` header. Verifying a proxy removes that limit for it; your plan's caps then apply alone. Proxies of accounts created any other way never have this limit. - **The verified mark.** Your proxy's manifest (`service` block) and its registry entry carry `verified`, `verification_method`, `verified_at`, and `last_checked_at`. Buyers and their agents read these fields to tell a proxy run by the API's owner from one run by someone else. `verified_at` means verified since: it's when the current method first found your token, and later successful checks don't move it. `last_checked_at` is when the latest check ran, including the daily re-check. Both are UTC to the second, ending in `Z`. A [grandfathered](#proxies-listed-before-verification) listing shows `verified: false` with `verification_method: "grandfathered"`: it's listed because it predates verification, and nothing was proven. Verification belongs to the target it was proven on. If you change a proxy's target URL, the proxy becomes unverified and is unlisted; see [Change the target URL](#change-the-target-url). ## Before you start You need: - A Lightning Enable merchant API key, sent as the `X-API-Key` header on every call below. You received it when you created your account: in the `apiKey` field of the one-payment claim response, or in **Dashboard → Settings → API Credentials**, where you can also regenerate it. - The proxy's ID, for example `forecast-api-a1b2`. Find it in `proxy.proxyId` in the claim response, in the response of `POST /api/proxy` that created the proxy, on the dashboard's proxy list, or by listing your proxies with `GET /api/proxy`. The examples on this page use a proxy whose target base URL is `https://api.example.com/v1`. - The ability to change one of these: the files served at the target host's web root, the DNS records for the target's hostname, or the API's response headers. All calls go to `https://api.lightningenable.com`. ## Step 1: Get a verification token If you created the proxy with the one-payment claim, the claim response already contains a token in `verification.token`. Use it and skip to [step 2](#step-2-publish-the-token). Otherwise, issue a token: ```bash curl -X POST https://api.lightningenable.com/api/proxy/forecast-api-a1b2/verification/token \ -H "X-API-Key: $LIGHTNING_ENABLE_API_KEY" ``` The response's `token` field holds the token: a 43-character string of letters, digits, `-`, and `_`. **This is the only time you see it.** Lightning Enable stores only a hash of it, so it can't show it to you again. If you lose it, issue a new one. Keep these rules in mind: - The token proves ownership for listing purposes only. It can't call the API, move money, or change your account. It's still a credential: don't share it beyond the place you publish it. - Issuing a new token replaces the old one. The old token stops working, and the proxy becomes unverified and unlisted until you publish the new token and verify again. The one exception is a [grandfathered proxy](#proxies-listed-before-verification), which stays verified and listed. - Each proxy has its own token. Two proxies on the same host need two tokens. ## Step 2: Publish the token Choose one method, publish the token, and go to step 3. You don't need more than one. ### Verification methods Lightning Enable tries the methods in this order and stops at the first one that finds your token. The method name is the value you see in the `method` field after a successful check. | Method | Where the token goes | Choose it when | |---|---|---| | `well-known` | A JSON file at `https://{target host}/.well-known/lightning-enable.json` | You control the web server for the target's hostname and can add a file at its root. | | `dns-txt` | A DNS TXT record named `_lightning-enable.{target host}` | Someone else hosts the API, but you manage DNS for its hostname (a custom domain on a hosting platform, for example). | | `header` | An `X-Lightning-Enable-Verify` response header at the target's base URL | You own the API's code but not the web root or DNS, for example an API mounted under a path on a shared domain. | Each method proves something different. The file proves that you control the web root of the target host, the TXT record proves that you control the DNS zone for the target's hostname, and the header proves only that you control the API's responses at the target base URL. Buyers see which method you used in `verification_method`. In every example, replace `` with your token. The *target host* is the hostname of the proxy's target base URL, including any subdomain: for `https://api.example.com/v1`, it's `api.example.com`, not `example.com`. #### Well-known file Serve this file over HTTPS on port 443, with a valid, publicly trusted certificate for the target host, at the root of that host. Lightning Enable fetches it over HTTPS even when your target base URL uses `http`. The path is the same whatever path your target base URL has. URL: ```text https://api.example.com/.well-known/lightning-enable.json ``` Body: ```json {"verify":""} ``` To verify several proxies that point at the same host, list every token in an array: ```json {"verify":["",""]} ``` The file must answer `200` (any `2xx` status works), be valid JSON, and stay under 4 KB. #### DNS TXT record Add one TXT record: | Field | Value | |---|---| | Name | `_lightning-enable.api.example.com` | | Type | `TXT` | | Value | `` | In zone-file form: ```text _lightning-enable.api.example.com. 300 IN TXT "" ``` The value is the token and nothing else: no prefix such as `lightning-enable-verify=`. Several TXT records can share the name, which lets you verify several proxies on one host. Many DNS providers add your domain to the name you enter. If your zone is `example.com`, enter `_lightning-enable.api` in the name field; entering the full name can create `_lightning-enable.api.example.com.example.com`. #### Response header Return this header from the target's base URL, with a trailing slash (`https://api.example.com/v1/` for the example proxy). Lightning Enable reads the base URL's scheme, host, and path only: if the target URL has a query string or a fragment, it isn't sent. ```text X-Lightning-Enable-Verify: ``` Lightning Enable sends `HEAD` first and then `GET` to that URL. Any status counts except a redirect (`3xx`), so an API root that answers `401` or `404` works, as long as the header is on the response. Lightning Enable reads only the headers, never the body. To carry several tokens, separate them with commas. ## Step 3: Run the check ```bash curl -X POST https://api.lightningenable.com/api/proxy/forecast-api-a1b2/verify \ -H "X-API-Key: $LIGHTNING_ENABLE_API_KEY" ``` A successful check looks like this (the `instructions` block is shortened here; see the [API reference](./producer-api-reference#proxy-ownership-verification) for all of it): ```json { "proxyId": "forecast-api-a1b2", "verified": true, "method": "dns-txt", "verifiedAt": "2026-11-08T18:02:11Z", "lastCheckedAt": "2026-11-08T18:02:11Z", "failures": 0, "tokenIssued": true, "token": null, "listed": false, "nextCheckAt": "2026-11-09T04:00:00Z", "checkedAt": "2026-11-08T18:02:11Z", "reason": null, "pending": false, "retryAfterSeconds": null, "instructions": { "...": "..." } } ``` `verified: true` means the check found your token, and `method` says how. `listed` stays `false` until you turn on listing yourself: ```bash curl -X PUT https://api.lightningenable.com/api/proxy/forecast-api-a1b2/manifest/settings \ -H "X-API-Key: $LIGHTNING_ENABLE_API_KEY" -H "Content-Type: application/json" \ -d '{"manifestEnabled": true, "manifestPubliclyListed": true}' ``` ### Read a failed check When no method finds the token, `verified` is `false` and `reason` explains what each method saw, in order, separated by semicolons: ```json { "verified": false, "failures": 1, "reason": "well-known: /.well-known/lightning-enable.json answered HTTP 404; dns-txt: no TXT record at _lightning-enable.api.example.com; header: HEAD answered HTTP 401 without X-Lightning-Enable-Verify; GET answered HTTP 401 without X-Lightning-Enable-Verify" } ``` Look at the part for the method you chose; the other two failing is expected. The reason never contains your token. Fix what it names and call `verify` again. If the reason starts with `The target host was refused before any request`, no method ran; see [Private or internal host](#private-or-internal-host). ### A pending check Each check has an overall time limit. If your target is slow, the check can run past it. Then the response has `pending: true` and `retryAfterSeconds` (also sent as a `Retry-After` header), and `reason` says what the check learned before it stopped. A pending check records nothing: it doesn't count as a failure. Wait the number of seconds given and call `verify` again. ### Check status without running a check ```bash curl https://api.lightningenable.com/api/proxy/forecast-api-a1b2/verification \ -H "X-API-Key: $LIGHTNING_ENABLE_API_KEY" ``` This returns the same state and the exact `instructions` for this proxy (file URL, TXT record name, header name and URL), with `` in place of the token. It never returns the token itself. Both `POST` calls allow 10 requests per minute per API key. ## Stay verified Lightning Enable re-checks every active, verified proxy once a day, at 04:00 UTC; `nextCheckAt` shows the next run. `nextCheckAt` is `null` when the proxy isn't re-checked: while it's unverified or inactive, and for `grandfathered` and `platform` proxies. Leave the token in place for as long as you want the proxy to stay verified. - A passing re-check resets `failures` to `0`. - A failing re-check adds one to `failures`. The proxy stays verified. - When a daily re-check finds **three consecutive failed checks**, the proxy becomes unverified: `verified` turns `false`, the proxy is unlisted from the registry, agent discovery, and Nostr, and a claimed proxy is limited again. The proxy itself keeps working by its direct URL; Lightning Enable never deletes or deactivates it. To recover, publish the token again (or [issue a new one](#step-1-get-a-verification-token)), call `verify`, and turn listing back on with `PUT /api/proxy/{proxyId}/manifest/settings`. Listing doesn't come back by itself. The daily re-check has a longer time limit than the `verify` endpoint, long enough for a slow server to answer. A re-check that still gets no answer within it counts as a failure, so a server that stops responding loses verification within three days, like one that answers with the wrong token. A failed `verify` call counts toward the three as well, so it can happen sooner. ### Proxies listed before verification A proxy that was already publicly listed when ownership verification was introduced stays listed. Its `method` (and `verification_method` in the manifest and registry) is `grandfathered`, and `verified` is `false`: it's listed because it predates verification, not because ownership was proven. The daily re-check skips it, so it never loses its listing that way, and issuing a token doesn't unlist it. Changing its target URL does: see [Change the target URL](#change-the-target-url). `grandfathered` isn't a method you can choose. To replace it with a real one, issue a token, publish it by one of the [three methods](#verification-methods), and call `verify`. Until a check succeeds, the proxy keeps its grandfathered standing. ### Shopify store listings Shopify store entries that Lightning Enable operates are verified by the platform, because their target is Lightning Enable's own host, not a server you run. Their `method` (and `verification_method`) is `platform`, and `verified` is `true`. Like `grandfathered`, `platform` isn't a method you can choose, and the daily re-check skips it. Shopify entries that were already listed when ownership verification was introduced were stamped `grandfathered` at first, and they're upgraded to `platform` the next time Lightning Enable syncs the store's registry entry. That sync runs when the service starts and whenever the store's listing changes. Delisting and relisting an entry doesn't change this. ### Change the target URL Verification proves control of one target. When you change a proxy's target URL with `PUT /api/proxy/{proxyId}` and a new `targetBaseUrl` (or with any later dashboard control that edits the target), Lightning Enable clears its verification and unlists it, whatever the method, `grandfathered` included. Only `platform` entries keep their standing. The proxy keeps working by its direct URL, and your token stays valid: publish it for the new target, call `verify`, and turn listing back on with `PUT /api/proxy/{proxyId}/manifest/settings`. A change that only adds or removes a trailing slash doesn't count. ## Troubleshooting ### Private or internal host Reason: `The target host was refused before any request: ...` Lightning Enable checks only public hosts. A target that resolves to a private, loopback, or link-local address, or a hostname that doesn't resolve, is refused before any request or DNS TXT lookup. The proxy can't forward to that host either, so fix the target: point the proxy at the API's public HTTPS address. ### Redirects Reason: `answered with a redirect (HTTP 301), which is not followed` Lightning Enable never follows redirects, even one that would lead to the token. Common causes: - The target host redirects `/.well-known/...` to another host (apex to `www`, or to a docs site). Serve the file on the target host itself. - The target base URL lacks a trailing slash and the server redirects `/v1` to `/v1/`, or the reverse. Make the base URL answer directly with the header. - An authentication layer redirects to a sign-in page. Exempt `/.well-known/lightning-enable.json` (or the `HEAD` request to the base URL) from it. ### A CDN or cache serves an old answer If a CDN sits in front of the target, it can keep serving a cached `404` from before you added the file, or an old file after you [issue a new token](#step-1-get-a-verification-token). The reason then says the file answered `404` or `"verify" does not contain this proxy's current token`. Purge `/.well-known/lightning-enable.json` from the cache, or serve it with `Cache-Control: no-cache`. Headers can be cached the same way at the base URL. ### DNS changes haven't propagated Reason: `no TXT record at _lightning-enable.api.example.com` A new record can take a few minutes to reach every resolver, and a changed record can take as long as the old record's TTL. Check what public DNS returns: ```bash dig +short TXT _lightning-enable.api.example.com ``` ```bash nslookup -type=TXT _lightning-enable.api.example.com ``` If the command returns nothing, wait and try again. A resolver that looked up the name before the record existed can also cache that "no record" answer for a while (the negative-caching time set in your zone's SOA record), so a lookup soon after an early failed check can stay empty for longer than the new record's TTL. If it returns a different value, look for a record that your provider created under a longer name (see [DNS TXT record](#dns-txt-record)) or an old token. ### The check times out Reason: `no answer within ...s`, or a response with `pending: true` Each request has its own short timeout, and the whole check has an overall limit. A target that is slow to answer, or a DNS server that is slow to respond, can run past either one. Make sure the well-known file or the base URL answers quickly without waking a cold-start backend, then call `verify` again after `retryAfterSeconds`. If one method is slow, use another: a DNS TXT record doesn't depend on your server's response time. ### Connection and certificate errors Reason: `could not be reached (...)` or `failed (...)` Lightning Enable couldn't complete the request. The word in parentheses names the cause: for example `NameResolutionError` (the hostname doesn't resolve), `ConnectionError` (nothing answered on the port, or the connection was refused), or `SecureConnectionError` (the TLS handshake failed, often an expired, self-signed, or wrong-host certificate). A `failed (...)` reason names an unexpected error type. Check that the host answers HTTPS on port 443 with a valid certificate, for example with `curl -I https://api.example.com/.well-known/lightning-enable.json`, and that a firewall doesn't block requests from outside your network. ### The target URL can't be verified Reason: `The target URL cannot be verified: ...` The proxy's target base URL itself is not one Lightning Enable checks, so no method ran. The rest of the reason says why: the URL uses an IP address instead of a hostname, points at `localhost` or an internal domain, uses a port other than 80 or 443, or uses a scheme other than `http` or `https`. Update the proxy's target to the API's public hostname, then issue a token and verify again. ### Other reasons | Reason contains | What to do | |---|---| | `is not valid JSON` | Serve exactly `{"verify":""}`, with straight quotes and no comments. | | `has no "verify" field` | The key must be `verify`, in lowercase. | | `is larger than 4096 bytes` | Serve only the `verify` field, not a larger document. | | `/.well-known/lightning-enable.json answered HTTP 404` | The file isn't at the host root. Check the path and that it's served on the target host itself. | | `"verify" must be a string or an array of strings` | Put the token in quotes, or in an array of quoted tokens. | | `"verify" does not contain this proxy's current token` | The file's token is old or mistyped. Publish the token you most recently issued, or issue a new one. | | `no TXT record at _lightning-enable.api.example.com contains this proxy's current token` | A TXT record exists at the name, but none of its values is exactly your current token. Remove quotes, prefixes, and old tokens, or [issue a new token](#step-1-get-a-verification-token). | | `answered HTTP 401 without X-Lightning-Enable-Verify` | The base URL answered, but without the header. Add it to every response at the base URL, including error responses and `HEAD`. | | `X-Lightning-Enable-Verify does not carry this proxy's current token` | The header is there, but its value isn't your current token. Publish the token you most recently issued. | | `the host check did not finish within ...s` or `the host check failed (...)` | Lightning Enable couldn't look up the target host in time. Try again; if it persists, check the hostname's DNS. | | `No verification token` (`409`, `verification_token_required`) | No token exists for this proxy. [Issue one](#step-1-get-a-verification-token). | ## Next steps - [Sell With Your Agent](/getting-started/sell-with-your-agent): create a paid proxy end to end. - [Producer API Reference](./producer-api-reference#proxy-ownership-verification): full request and response fields for the verification endpoints. - [Proxy Configuration](./proxy-configuration): manifest settings, including public listing. ============================================================================== # Product Overview Source: https://docs.lightningenable.com/products/product-overview ============================================================================== # Lightning Enable Products Lightning Enable offers a **Free Producer Sandbox** to start with no card, a self-serve **Agentic Commerce** plan, and **Agentic Commerce — Business** for teams that want white-glove onboarding. All plans use the **BYOA (Bring Your Own API Key)** model — you bring your own **Strike** API key (OpenNode on Business), or receive into your own wallet over [NWC](/nwc-setup/account-setup), and funds flow directly to your account. ## Plans | Feature | Free Producer Sandbox | Agentic Commerce | Agentic Commerce — Business | |---------|------------------------|-----------|------------------------| | **Price** | Free — no card | $49/month | [Contact us](mailto:support@lightningenable.com) | | **Free Trial** | N/A — Free is the floor | 30 days, self-serve checkout | Arranged on contact | | **Best For** | Hobbyists, demos, side projects | Individual developers | Platforms and teams monetizing APIs, e-commerce, and agent commerce | | **REST API** | ✅ | ✅ | ✅ | | **checkout.js Widget** | ✅ | ✅ | ✅ | | **Webhook Integration** | 1 endpoint | ✅ | ✅ | | **Multi-Currency** | ✅ | ✅ | ✅ | | **L402 Protocol** | ✅ (capped) | ✅ | ✅ | | **Pay-per-Request** | ✅ (capped) | ✅ | ✅ | | **MCP Server** | ✅ | ✅ | ✅ | | **L402 Producer API** | ✅ | ✅ | ✅ | | **White-glove Onboarding** | ❌ | ❌ | ✅ | Free is capped rather than unlimited: 3 L402 endpoints, 200 challenges per month, 1,000 sats maximum per challenge, and 1 proxy configuration. You can pay for Agentic Commerce by card ($49/month, with a 30-day trial) or in sats over L402, 30 days per payment at the price the renewal quote (`GET /api/merchant/subscription/renew`) returns. See [Renew in sats](/getting-started/activate-with-lightning#renew-in-sats). ## Platform Integrations (Included with Any Paid Plan) Platform integrations extend your Agentic Commerce plan with native e-commerce platform support at no additional cost. Not included on the Free Producer Sandbox. | Integration | Description | |-------------|-------------| | **[Shopify Commerce](/products/shopify-commerce/overview)** | L402 agentic commerce for Shopify stores — AI agents browse, pay, and order | ## Architecture All Lightning Enable products share the same architecture: ``` ┌─────────────┐ ┌───>│ Strike │───> Lightning Network Your Platform ──> Lightning Enable ┤ │ (provider) │ (settlement) (middleware) │ └─────────────┘ │ ┌─────────────┐ └───>│ OpenNode │───> Lightning Network │ (provider) │ (settlement) └─────────────┘ ``` **Key points:** - Lightning Enable is API middleware — we never touch your funds - Your chosen provider (Strike or OpenNode) facilitates custody and settlement - You bring your own API key (BYOA model) — choose Strike or OpenNode per merchant - Flat subscription pricing — no per-transaction fees --- :::tip 30-Day Free Trial Agentic Commerce includes a **30-day free trial** with full API access through self-serve checkout. A card is required upfront but you will not be charged until the trial ends. One trial per email. Agentic Commerce — Business is contact-only ([email support@lightningenable.com](mailto:support@lightningenable.com)) — it isn't purchasable through self-serve checkout, and any trial terms are arranged directly. Prefer no card at all? Start with the [Free Producer Sandbox](https://api.lightningenable.com/dashboard/signup) or the no-card [L402 Fast Lane](/getting-started/activate-with-lightning). See [Free Trial](/products/subscription-management#free-trial) for details. ::: ## Free Producer Sandbox **Free — no card required** | [Sign up](https://api.lightningenable.com/dashboard/signup) Capped L402 access for hobbyists, demos, and side projects. Upgrade to Agentic Commerce when you have real traffic. ### Ideal For - **Hobbyists and demos** - Prove L402 works before you commit to anything - **Side projects** - Test payment flows without a card - **Evaluators** - Kick the tires before recommending Lightning Enable to a team ### What You Get - 3 L402 endpoints - 200 challenges / month - 1,000 sats max per challenge - 1 proxy config - Manifest generation - Community support [Sign Up →](https://api.lightningenable.com/dashboard/signup) --- ## Agentic Commerce **$49/month** | [Documentation](/products/agentic-commerce/overview) Full L402 access for individual developers with settlement via Strike API. The fastest way to start building with Lightning payments. ### Ideal For - **Individual developers** - Build and test L402 integrations - **Side projects** - Monetize APIs with Lightning payments - **AI builders** - Connect agents to paid APIs via MCP ### What You Get - Unlimited L402 endpoints - Strike as settlement provider - Per-endpoint pricing - Live dashboard + per-request payment feed [Get Started →](/products/agentic-commerce/overview) --- ## Agentic Commerce — Business **[Contact us](mailto:support@lightningenable.com)** | [Documentation](/products/agentic-commerce/overview) Pay-per-request API monetization using the L402 protocol, with white-glove onboarding. Ideal for AI services, premium APIs, and machine-to-machine payments. ### Ideal For - **API providers** - Charge per inference, per token, per request - **Premium data APIs** - Weather, financial, research data - **Content APIs** - News, media, entertainment - **Developer tools** - CI/CD, testing, monitoring services - **MCP server operators** - Monetize AI agent capabilities ### What You Get - Everything in Agentic Commerce - White-glove onboarding - Direct founder access (no ticket queues) - Strike or OpenNode as settlement provider ### How L402 Works ``` 1. Client requests protected endpoint 2. Server returns HTTP 402 with Lightning invoice 3. Client pays invoice 4. Client includes proof-of-payment in header 5. Server grants access ``` ### Quick Example ```bash # Request without payment -> 402 Payment Required curl https://api.yourservice.com/premium-data # Response: 402 with invoice + macaroon # Pay invoice with Lightning wallet, get preimage # Request with L402 credential -> 200 OK curl https://api.yourservice.com/premium-data \ -H "Authorization: L402 :" ``` [Contact Sales →](mailto:support@lightningenable.com) --- ## Pricing Philosophy **No per-transaction fees.** Your subscription is based on capabilities, not usage. | What We Charge For | What We Don't Charge For | |--------------------|-----------------------| | API access | Transaction volume | | Feature tier | Payment amounts | | Support level | Number of invoices | | Integration depth | API calls | Your payment provider may charge their own processing fees.* These go to the provider, not to us. *Check your provider's current fee schedule for details. --- ## Subscription Enforcement All plans are enforced via middleware on every API request. If your subscription expires, is canceled, or your payment fails, API requests return `403 Forbidden` with a clear error message and recommended action. Key behaviors: - **Active** and **trialing** subscriptions have full API access - **Past due**, **canceled**, and **unpaid** subscriptions are blocked immediately - **Billing period validation** catches expired subscriptions even before Stripe webhooks arrive - **Feature gating** restricts plan-specific features (e.g., Shopify Commerce requires a paid plan) - **Free Producer Sandbox accounts** have API access without a subscription, capped at 3 endpoints and 200 challenges per month For complete details, see [Subscription & Plan Enforcement](/products/subscription-management). --- ## Getting Started 1. **Choose your product** based on your platform and needs 2. **Sign up** — the [Free Producer Sandbox](https://api.lightningenable.com/dashboard/signup), Agentic Commerce by [card trial](https://api.lightningenable.com/Checkout?plan=individual) or [L402 Fast Lane](/getting-started/activate-with-lightning), or Agentic Commerce — Business by contacting support@lightningenable.com 3. **Set up your payment provider** — [Strike](/strike-setup/account-setup) (recommended) or [OpenNode](/opennode-setup/account-setup) 4. **Configure your API key** in the [dashboard](https://api.lightningenable.com/dashboard) → Settings 5. **Integrate** using our docs and examples Questions? Email [support@lightningenable.com](mailto:support@lightningenable.com). ============================================================================== # Run your own Lightning store Source: https://docs.lightningenable.com/products/run-your-own-store ============================================================================== # Run your own Lightning store :::tip Subscription required The store API needs a paid plan: **Agentic Commerce** ($49/month) or **Business** ([contact us](mailto:support@lightningenable.com)). The Free Producer Sandbox doesn't include it. [View pricing](/products/product-overview) ::: The Lightning Enable store is a storefront that sells print-on-demand merchandise for Lightning payments, to people and to AI agents. You can run your own copy of it, with your own catalog, your own [Printful](https://www.printful.com/) account, and your own Lightning Enable account. Buyers pay Lightning invoices that your payment provider settles; Lightning Enable does not hold funds. The storefront app is a separate, self-hosted application. Lightning Enable stores its data (orders, custom-product designs, quotes, listings, and community submissions) through the store API on this page. Setup guides for the storefront app itself are coming. ## How your data is kept separate Every store record belongs to one Lightning Enable account. When your storefront calls the store API with your API key, it reads and writes only your records: - Another account's order, design, quote, artwork job or submission answers `404 Not Found`, whether you look it up by id, payment hash, claim token or idempotency key. - Order ids, design ids, idempotency keys and claim tokens only need to be unique within your account. - Lightning payment hashes are unique across all accounts, because a payment hash identifies one Lightning payment. Binding a hash that is already in use returns `409 payment_hash_in_use`. ## Authenticate Send your merchant API key in the `X-API-Key` header, the same key you use for `POST /api/payments`: ```bash curl https://api.lightningenable.com/api/store-orders/ord_123 \ -H "X-API-Key: YOUR_API_KEY" ``` The store API covers these route prefixes: | Prefix | What it stores | |--------|----------------| | `/api/store-orders` | Catalog and custom-product orders, and each order's timeline | | `/api/store-designs` | Custom-product designs, artwork versions, mockups and artwork jobs | | `/api/store-quotes` | Custom-product price quotes | | `/api/store-settings` | Your store settings: design fee, pricing, publishing rules and offered catalog | | `/api/store-listings` | Published designs (listings) | | `/api/community-submissions` | Community design submissions | An account on the Free Producer Sandbox gets `403` with `"error": "plan_required"`. See [Plan Required (store API)](/api-reference/errors#plan-required-store-api). The store API has its own rate budget of 600 requests per minute per API key, separate from the budget for your other API calls such as `POST /api/payments`. Over the budget, you get `429 Too Many Requests`. ## Design files Community design files and previews that you upload through `/api/community-submissions/{submissionId}/design-file` and `/preview` are stored under your account, so another account's submission with the same id never shares your files. A submission id used with these routes can contain only `A-Z`, `a-z`, `0-9`, `_` and `-`. Deleting a submission deletes its files. ## Published designs A buyer's approved custom design can become a listing that other buyers purchase. Publishing is off until you turn it on with `PUT /api/store-settings` (`publishingEnabled: true`). The same settings control review (`publishingRequiresReview`, on by default), open listings per designer (`maxPublishedPerDesigner`, default 5), listing lifetime (`listingExpiryDays`, 0 = never expires), and the designer's share of each sale (`designerRevenueSharePercent`, 0 to 50). - **Request:** `POST /api/store-designs/{designId}/publication` with `{"action": "request"}`. Only an `Approved` design qualifies. With review on, it waits as `pending_review`. - **Review or take down a listing:** the same route with `{"action": "approve" | "reject", "reason": "...", "actor": "operator"}`. The reason is required and is never shown on a listing. - **Take down a design:** `POST /api/store-designs/{designId}/moderation-reject` with `{"reason": "...", "actor": "operator"}`. Its listing closes, open quotes are rejected, unpaid orders are cancelled, and paid orders not yet sent to Printful wait for your review. - **Sell:** buyers quote with `POST /api/store-quotes` using `listingId` instead of `designId`, then order with `POST /api/store-orders/custom`. Each listing order keeps the revenue share in effect when its quote was made. You pay any designer share from your own payment provider account. - **Browse:** `GET /api/store-designs` lists your designs for an admin page; `GET /api/store-listings` and `GET /api/store-listings/{listingId}` return listings. - A closed listing never reopens and answers `409 listing_not_available`. Turning `publishingEnabled` off pauses every listing sale with the same error (`"reason": "publishing_disabled"`) until you turn it back on. - Responses include each listing's `designId`, which your storefront needs internally. Show buyers only the `listingId`. ## Payments and claims Your storefront creates each Lightning invoice with `POST /api/payments` on your account, so payments settle into your payment provider account. When an invoice is paid, Lightning Enable moves only your own matching order to paid. Your storefront sends buyers their claim links; Lightning Enable sends no email to your buyers. ============================================================================== # Agent Discovery & Registry Source: https://docs.lightningenable.com/products/shopify-commerce/agent-discovery ============================================================================== # Agent Discovery & Registry When you create a Shopify integration with Lightning Enable, your store is **automatically registered** in the L402 API registry. This means AI agents can find your store using the `discover_api` MCP tool — no extra setup required. ## How It Works ``` AI Agent L402 Registry Your Store │ │ │ ├─ discover_api("coffee") ─────────►│ │ │◄──── matching stores ────────────┤ │ │ │ │ ├─ discover_api(url=manifest) ─────►│ │ │◄──── full endpoint details ──────┤ │ │ │ │ ├─ GET /catalog ────────────────────┼───────────────────────────────►│ │◄──── products ────────────────────┼───────────────────────────────┤ │ │ │ ├─ POST /checkout ──────────────────┼───────────────────────────────►│ │◄──── 402 + invoice ──────────────┤ │ │ ... (pay + claim flow) ... │ │ ``` 1. **Agent searches** the registry using keywords like "coffee", "health", or a category like "food-and-beverage" 2. **Registry returns** matching stores with name, description, categories, and manifest URL 3. **Agent fetches manifest** for full endpoint details — what's free, what requires payment, pricing model 4. **Agent uses the store** — browses the catalog, creates a checkout, pays, retries the checkout with the L402 proof, and claims ## Registry Listing By default, `listInRegistry` is enabled when you create a Shopify integration. Your store appears in the registry with: - **Name** — derived from your slug (e.g., "my-store (Shopify)") - **Description** — your `registryDescription` or a default description - **Categories** — your `registryCategories` for keyword and category search - **4 endpoints** — catalog (free), checkout (L402/dynamic), claim (L402), order status (free) ### Categories Categories help agents find your store. Use a JSON array of lowercase, hyphenated terms that describe what you sell: ```json ["commerce", "food-and-beverage", "health", "hydration"] ``` Good categories are specific and domain-appropriate. Some examples: | Store Type | Suggested Categories | |------------|---------------------| | Food & drink | `commerce`, `food-and-beverage`, `health`, `nutrition` | | Clothing | `commerce`, `apparel`, `fashion` | | Electronics | `commerce`, `electronics`, `gadgets` | | Art & prints | `commerce`, `art`, `digital-goods` | | Software licenses | `commerce`, `software`, `digital-goods` | ### Description The `registryDescription` is used for keyword search. Write it like you're describing your store to an AI agent that needs to decide whether to shop there: ``` Premium single-origin coffee beans and brewing equipment. Specialty roasts from Ethiopia, Colombia, and Guatemala available for AI agent purchases via Lightning L402 payments. ``` ## Configuring Registry Settings ### At Creation Include registry fields when creating your integration (credentials come from [Setup Step 1](/products/shopify-commerce/setup#step-1-create-a-custom-app-for-your-store)): ```bash curl -X POST https://api.lightningenable.com/api/merchant/shopify \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "slug": "my-coffee-store", "shopifyDomain": "my-store.myshopify.com", "shopifyClientId": "YOUR_CLIENT_ID", "adminApiAccessToken": "shpss_YOUR_CLIENT_SECRET", "listInRegistry": true, "registryCategories": "[\"commerce\",\"food-and-beverage\",\"coffee\"]", "registryDescription": "Premium single-origin coffee beans and brewing equipment available for AI agent purchases via Lightning L402 payments." }' ``` ### Updating Update registry fields at any time: ```bash curl -X PUT https://api.lightningenable.com/api/merchant/shopify \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "registryCategories": "[\"commerce\",\"food-and-beverage\",\"coffee\",\"specialty-roasts\"]", "registryDescription": "Updated description for your store." }' ``` ### Configuration Reference | Field | Type | Default | Description | |-------|------|---------|-------------| | `listInRegistry` | boolean | `true` | Whether to list your store in the L402 API registry | | `registryCategories` | string | `["commerce"]` | JSON array of categories for search and filtering | | `registryDescription` | string | auto-generated | Rich description for keyword search | ## Disabling Registry Listing To remove your store from agent discovery while keeping the integration active: ```bash curl -X PUT https://api.lightningenable.com/api/merchant/shopify \ -H "X-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"listInRegistry": false}' ``` Your store endpoints still work — agents with the direct URL can still purchase. They just won't find you via `discover_api` search. :::info Pausing vs Delisting Setting `isActive: false` **pauses** the integration entirely (no purchases possible) and also removes it from the registry. Setting `listInRegistry: false` only hides it from discovery — agents with the URL can still shop. ::: ## The Manifest Each registered store gets an auto-generated L402 manifest at: ``` https://api.lightningenable.com/l402/proxy/shopify-{slug}/.well-known/l402-manifest.json ``` The manifest follows the [L402 Manifest Schema](/schemas/l402-manifest-v1.json) and includes: - **Service metadata** — name, description, categories, documentation URL - **L402 configuration** — payment flow, supported currencies - **Endpoints** — each with path, method, pricing model, and whether L402 is required ### Endpoint Pricing Models | Endpoint | Pricing | L402 Required | Description | |----------|---------|---------------|-------------| | `GET /catalog` | Free | No | Browse products — always free | | `POST /checkout` | Dynamic | Yes | Price depends on cart contents | | `POST /claim` | Free | Yes | Submit shipping details with the claim token; the L402 credential is needed only if the order isn't marked paid yet | | `GET /orders/{id}` | Free | No | Check order status with claim token | "Dynamic" pricing means the price is determined at request time based on what's in the cart. The agent sees this in the manifest and knows to expect a variable amount. ## How Agents Use discover_api AI agents using the [Lightning Enable MCP server](/products/agentic-commerce/mcp-quickstart) can search the registry in two ways: ### Keyword Search ``` Agent: "Find stores that sell coffee" → discover_api(query="coffee") → Returns matching stores with names, descriptions, categories, and manifest URLs ``` ### Category Browse ``` Agent: "What food and beverage stores are available?" → discover_api(category="food-and-beverage") → Returns all stores in that category ``` ### Manifest Fetch ``` Agent: "Show me the full details for this store" → discover_api(url="https://api.lightningenable.com/l402/proxy/shopify-my-store/.well-known/l402-manifest.json") → Returns complete endpoint details, pricing models, and L402 requirements ``` ### Budget-Aware Discovery With `budgetAware: true` (the default), `discover_api` annotates results with how many calls the agent can afford on its remaining budget. Catalog browsing is free. ## Next Steps - [Setup Guide](/products/shopify-commerce/setup) — Create your Shopify integration - [API Reference](/products/shopify-commerce/api-reference) — Full endpoint documentation - [MCP Quick Start](/products/agentic-commerce/mcp-quickstart) — Set up AI agents to discover and purchase ============================================================================== # API Reference Source: https://docs.lightningenable.com/products/shopify-commerce/api-reference ============================================================================== # 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`** ```json { "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:** | Status | Meaning | |--------|---------| | `404` | Integration 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):** | Header | Format | Description | |--------|--------|-------------| | `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:** ```json { "items": [ { "variantId": 44567890123456, "quantity": 2 }, { "variantId": 44567890123457, "quantity": 1 } ], "email": "customer@example.com" } ``` | Field | Required | Description | |-------|----------|-------------| | `items` | Yes | Array of `{ variantId, quantity }` (1–10 items) | | `email` | No | Buyer email (max 200 characters). When set, the buyer is emailed the claim URL once the payment lands. **This is the recovery path when the buyer's wallet returns no preimage** (for example, some Strike-to-Strike payments), so the [checkout retry](#complete-a-paid-checkout-checkout-retry) isn't possible. Pass it when buying for a human. | **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:** ```json { "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 calculated by Shopify from the buyer's `X-Buyer-Location` - `taxNote` — always `null` - `totalUsd` — total including subtotal + shipping + tax :::info 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](#complete-a-paid-checkout-checkout-retry) below. ::: **Tax Calculation:** Lightning Enable creates a temporary Shopify draft order with the cart and the buyer's location, reads the tax Shopify computes, and deletes the draft. The tax is included in the invoice total. **Error Responses:** | Status | Meaning | |--------|---------| | `400` | Invalid cart (bad variant ID, unavailable item, exceeds limits), missing/blank `X-Buyer-Location`, or a payment credential for a disabled protocol | | `404` | Integration not found | | `429` | Too many checkout requests. Wait a minute and retry. | | `503` | Bitcoin price feed unavailable (no invoice is issued at a stale rate; includes a `correlationId`), tax calculation failed, or no invoice could be issued (`challenge_persist_failed`, `payment_provider_not_configured`, `payment_provider_busy`). Nothing was invoiced. Retry shortly, or tell the store owner if it persists. | --- ### 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 is validated before the credential (an empty `{"items": []}` returns `400`), but its content doesn't affect the retry: the payment credential identifies the paid order. **Response: `200 OK`** ```json { "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 (appended with `&` if the page URL already has a query string). 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](#claim-order) directly. - `claimToken` — the raw claim token. Present only when the merchant has **not** configured a claim page (so `claimPageUrl` is `null`); otherwise `null`, because the token is already in `claimPageUrl`. Use it to call [Claim Order](#claim-order) directly, and keep it: it's also the credential for [Get Order Status](#get-order-status). This retry response is the only place the raw token appears. If the order is already claimed, the token is still returned for status lookups and `message` says the order is claimed; if the claim window has expired, it's `null` and `message` says so. - The response is sent with `Cache-Control: no-store` because it carries the claim token. - 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. If the store has no claim page, the email carries the claim code instead of a link, with instructions to reply to the store with it or have an agent call [Claim Order](#claim-order). **Error Responses:** | Status | Meaning | |--------|---------| | `400` | Invalid request body (empty `items` or more than 10 items), credential for a disabled protocol, or invalid preimage format | | `401` | L402 verification failed (macaroon/preimage don't verify) | | `404` | No 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` — 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](#complete-a-paid-checkout-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:** ```json { "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" } } ``` `claimToken` and `email` are required (`email` max 200 characters). **Shipping Address Fields:** | Field | Required | Max Length | |-------|----------|-----------| | `firstName` | Yes | 200 | | `lastName` | Yes | 200 | | `address1` | Yes | 500 | | `address2` | No | 500 | | `city` | Yes | 100 | | `province` | No | 100 | | `zip` | No | 20 | | `country` | Yes | 100 | | `phone` | No | 30 | **Response: `200 OK`** ```json { "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:** | Status | Meaning | |--------|---------| | `400` | Expired claim window, already claimed, missing payment proof, invalid preimage format, **failed L402 verification**, or international address on a domestic-only store | | `401` | Payment credential for a disabled protocol only (a failing macaroon/preimage returns `400`, not `401`) | | `404` | Unknown 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`** ```json { "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:** | Status | Meaning | |--------|---------| | `404` | Order 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 OK`** — `text/plain; charset=utf-8`. Cached with the same TTL as the catalog. **Error Responses:** | Status | Meaning | |--------|---------| | `404` | Integration not found or inactive (also `text/plain`) | --- ## Merchant Admin Endpoints These endpoints require your Lightning Enable API key in the `X-API-Key` header. On a plan without the Shopify integration (the Free Producer Sandbox), they return `402` with `"error": "plan_feature_disabled"`. ### 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](/products/shopify-commerce/setup#step-5-create-the-shopify-integration) for the full request schema. ### Update Integration ``` PUT /api/merchant/shopify ``` Partial update — include only the fields you want to change. ```json { "domesticShippingUsd": 6.99, "freeShippingEnabled": true, "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:** ```json { "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:** | Parameter | Default | Description | |-----------|---------|-------------| | `status` | (all) | Filter by status: `PendingPayment`, `PaidAwaitingDetails`, `PaidWithDetails`, `Fulfilled`, `Shipped` | | `page` | 1 | Page number | | `pageSize` | 20 | Results per page (max 100) | **Response:** ```json [ { "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`: ```bash # 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 and the ORIGINAL body. # 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. # A store with no claim page returns the raw token as claimToken instead: CLAIM_TOKEN=$(echo "$RETRY" | jq -r '.claimToken // empty') if [ -z "$CLAIM_TOKEN" ]; then CLAIM_PAGE_URL=$(echo "$RETRY" | jq -r '.claimPageUrl') CLAIM_TOKEN="${CLAIM_PAGE_URL##*token=}" fi # 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 | Scenario | What Happens | |----------|-------------| | **BTC price moves after checkout** | The sats amount is locked in the Lightning invoice at checkout time. The invoice expires in ~10 minutes. | | **Product goes out of stock** | Checked at checkout time against cached catalog. At claim time, Shopify's `decrement_obeying_policy` handles inventory. | | **Claim token used twice** | Second claim is rejected (`400`, already claimed) — tokens are single-use for claiming. The token remains valid for order-status lookups. | | **Wallet returns no preimage** | The 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 fails** | Order stays in `PaidAwaitingDetails`. Payment is safe. Can be retried via admin. | | **BTC price feed unavailable** | Checkout returns `503` with a `correlationId` rather than quoting a stale rate. Retry shortly. | | **No tax location provided** | Checkout returns `400` if the required `X-Buyer-Location` header is missing or blank. There is no fallback. | | **Invoice expires before payment** | Order stays in `PendingPayment`. Agent must create a new checkout. | | **Claim window expires before claiming** | Contact the merchant for manual resolution. Payment is recorded. Default window is 30 days (configurable 1–365). | ============================================================================== # Overview Source: https://docs.lightningenable.com/products/shopify-commerce/overview ============================================================================== # Shopify Commerce :::tip Subscription Required Shopify L402 integration requires a paid plan: **Agentic Commerce** ($49/month) or **Agentic Commerce — Business** ([contact us](mailto:support@lightningenable.com)). The Free Producer Sandbox doesn't include it. [View pricing](/products/product-overview) ::: Lightning Enable's Shopify integration lets **AI agents purchase products** from your Shopify store using Lightning Network payments. Your existing Shopify catalog, inventory management, and fulfillment workflows stay exactly as they are — Lightning Enable adds a new sales channel powered by the L402 protocol. ## How It Works ``` AI Agent Lightning Enable Your Shopify Store │ │ │ ├─ GET /catalog ──────────────────►│──── Admin API products ────────────►│ │◄──── product list ──────────────┤◄──── products + prices ─────────────┤ │ │ │ ├─ POST /checkout ────────────────►│ │ │ (items + buyer location) │── create Lightning invoice ──► │ │◄──── 402 + invoice ─────────────┤ (via Strike/OpenNode) │ │ │ │ ├─ pay invoice ───────────────────►│ │ │◄──── preimage (proof) ──────────┤ │ │ │ │ ├─ POST /checkout (retry) ────────►│ │ │ (L402 payment proof) │ │ │◄──── claim URL + token ─────────┤ │ │ │ │ ├─ POST /claim ───────────────────►│ │ │ (claim token + shipping) │── POST /admin/api/orders.json ────►│ │◄──── order confirmation ────────┤◄──── Shopify order created ────────┤ │ │ (paid, inventory decremented) │ ``` 1. **Browse** — Agent fetches your product catalog, which Lightning Enable pulls from the Shopify Admin API and caches. 2. **Checkout** — Agent submits a cart with the buyer's location (required for tax), receives a Lightning invoice 3. **Pay** — Agent pays the invoice, receives cryptographic proof of payment (preimage) 4. **Retry** — Agent re-POSTs the same checkout endpoint with the L402 proof and receives the claim URL carrying the claim token (withheld before payment so nobody can race the buyer to claim) 5. **Claim** — Agent or human provides shipping details with the claim token, Shopify order is created ## Key Features - **Zero changes to your Shopify store** — products, inventory, and fulfillment stay in Shopify - **Auto-registered for agent discovery** — your store appears in the L402 API registry so AI agents can find it via `discover_api` - **Server-side pricing** — prices are always fetched from your Shopify catalog, never client-controlled - **Cryptographic payment proof** — L402 verification (SHA256(preimage) == payment_hash) confirms payment without webhook dependencies - **Automatic Shopify orders** — orders appear in your Shopify admin as "paid" with inventory decremented - **Automatic tax calculation** — tax computed via Shopify's Draft Order API using buyer location, included in the Lightning invoice total - **Flat-rate shipping** — configurable domestic and international rates with optional free shipping threshold - **Catalog caching** — products cached for configurable duration (default 15 minutes) to minimize API calls - **Claim tokens** — configurable claim window (default 30 days) for providing shipping details after payment ## What You Need 1. **Active Shopify store** — products you sell via L402 need **Active** status (Online Store publishing isn't required) 2. **Shopify app credentials** with `write_orders`, `write_draft_orders`, and `read_products` scopes 3. **A claim page** on your store where buyers submit shipping details (templates provided; see [Setup Step 7](/products/shopify-commerce/setup#step-7-add-the-claim-page-and-llmstxt-to-your-store)) 4. **Lightning Enable paid plan** — Agentic Commerce ($49/mo) or Agentic Commerce — Business ([contact us](mailto:support@lightningenable.com)) 5. **Payment provider account** — [Strike](https://dashboard.strike.me) (recommended; it returns the preimage L402 needs) ## Agent Flow vs Human Flow The Shopify L402 integration supports a two-phase flow: | Phase | Who | What Happens | |-------|-----|-------------| | **Purchase** | AI Agent | Browses catalog, creates checkout, pays Lightning invoice | | **Claim** | AI Agent or Human | Provides shipping address and email, triggers Shopify order creation | The **claim token** bridges the two phases. It is issued only after payment is proven: in the claim URL returned by the checkout retry, or emailed to the buyer when an email was given at checkout. Shipping details must be submitted within the claim window (default 30 days). After the claim, the same token works for order-status tracking. ## Order Lifecycle | Status | Meaning | |--------|---------| | `PendingPayment` | Checkout created, waiting for Lightning payment | | `PaidAwaitingDetails` | Payment confirmed, waiting for shipping details | | `PaidWithDetails` | Shipping details provided, Shopify order created | | `Fulfilled` | Shopify fulfillment started | | `Shipped` | Tracking info available | ## Next Steps - [Setup Guide](/products/shopify-commerce/setup) — Step-by-step integration setup - [API Reference](/products/shopify-commerce/api-reference) — Full endpoint documentation - [Agent Discovery & Registry](/products/shopify-commerce/agent-discovery) — Control how agents find your store ============================================================================== # Setup Guide Source: https://docs.lightningenable.com/products/shopify-commerce/setup ============================================================================== # Shopify Setup Guide This guide walks you through connecting your Shopify store to Lightning Enable for L402 agentic commerce. Setup takes about 10 minutes. ## Step 1: Create a Custom App for Your Store Lightning Enable uses Shopify's OAuth client-credentials flow to securely create orders in your store after Lightning payment. You create a custom app in your Shopify store admin (via the Dev Dashboard, which is Shopify's current workflow for merchant-created apps) and connect it to Lightning Enable using the app's Client ID and Client Secret. :::note No Partners account needed You launch the Dev Dashboard from your own store admin (Settings → Apps and sales channels → Develop apps → **Build apps**). The Partners Dashboard is for apps distributed to other merchants, which you don't need here. ::: 1. Sign in to your Shopify store admin 2. Go to **Settings → Apps and sales channels** 3. Click **Develop apps** 4. Click **Build apps** (this opens the Shopify Dev Dashboard) 5. In the Dev Dashboard, click **Create application** 6. Name the app `Lightning Enable` (or anything you prefer) 7. Under **Configuration → API scopes**, add the following Admin API access scopes: | Scope | Purpose | |-------|---------| | `write_orders` | Create orders after Lightning payment | | `write_draft_orders` | Create temporary draft orders for tax calculation | | `read_products` | Fetch your catalog, prices, and availability | 8. Click **Save** on the scope configuration 9. Go to the app's **Home** tab in the Dev Dashboard and click **Install app** → select your store → **Install** 10. After install, open the app's **API credentials** (or **Overview**) panel and copy: - **Client ID** - **Client Secret** (starts with `shpss_`) — Shopify reveals this value one time on this screen Keep this tab open — you'll paste both values into Lightning Enable in Step 5. :::caution Save Your Client Secret Shopify reveals the Client Secret only once on the API credentials screen after the app is installed. If you lose it, you can regenerate it from the same screen, but you'll need to update it in Lightning Enable afterward — the old secret stops working immediately on regeneration. ::: :::tip If your store already has a legacy custom app Shopify stopped allowing creation of new legacy custom apps in January 2026, but existing legacy custom apps still work. If you created yours before the cutoff, it's under Settings → Apps and sales channels → Develop apps (with no "Build apps" step), and you can use its existing Client ID and Client Secret. If you have to re-create for any reason, you'll go through the Dev Dashboard flow above. ::: ## Step 2: Verify Your Products Are Ready Lightning Enable fetches your catalog from the Shopify Admin API using the app credentials from Step 1. Make sure the products you want available via L402 are set up correctly: 1. Go to **Products** in Shopify admin 2. For each product you want available via L402: - Status should be **Active** — the Admin API catalog fetch filters to active products - Confirm inventory and pricing are accurate - Publishing to **Online Store** isn't required — the Admin API doesn't filter by sales channel ## Step 3: Sign Up for Lightning Enable If you don't already have a Lightning Enable account: 1. Visit [lightningenable.com](https://lightningenable.com) and sign up for **Agentic Commerce** ($49/month), or [contact us](mailto:support@lightningenable.com) about **Agentic Commerce — Business** 2. Complete the Stripe checkout 3. **Save your API key** from the confirmation page — you'll need it for the next step :::info Already Have a Subscription? If you're on the Free Producer Sandbox, which doesn't include the Shopify integration, upgrade through the [merchant dashboard](https://api.lightningenable.com/dashboard) or contact support. ::: ## Step 4: Configure Your Payment Provider You need a payment provider to handle Lightning invoice creation and settlement. ### Option A: Strike (Recommended) 1. Create a [Strike account](https://dashboard.strike.me) 2. Generate an API key in the Strike dashboard with the [scopes Lightning Enable needs](/strike-setup/account-setup#api-key-scopes) 3. Configure it in Lightning Enable: ```bash # Save your Strike API key (this also defaults you to the Strike provider on first save) curl -X PUT https://api.lightningenable.com/api/merchant/strike-key \ -H "X-API-Key: YOUR_LIGHTNING_ENABLE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"strikeApiKey": "YOUR_STRIKE_API_KEY"}' # Optional: explicitly set the payment provider (provider is a string, not an int) curl -X PUT https://api.lightningenable.com/api/merchant/payment-provider \ -H "X-API-Key: YOUR_LIGHTNING_ENABLE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"provider": "strike"}' ``` Strike supports preimage extraction, which is required for L402 payment verification. ### Option B: OpenNode See the [OpenNode Setup Guide](/opennode-setup/account-setup) for configuration details. :::warning L402 Compatibility OpenNode does **not** return preimages for incoming payments. For Shopify L402 integration, **Strike is strongly recommended** as it provides the preimage needed for cryptographic payment verification. ::: ## Step 5: Create the Shopify Integration With your Lightning Enable API key, create the integration using the Client ID and Client Secret you copied in Step 1: ```bash curl -X POST https://api.lightningenable.com/api/merchant/shopify \ -H "X-API-Key: YOUR_LIGHTNING_ENABLE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "slug": "your-store-name", "shopifyDomain": "your-store.com", "shopifyClientId": "YOUR_CLIENT_ID", "adminApiAccessToken": "shpss_YOUR_CLIENT_SECRET", "domesticShippingUsd": 5.99, "internationalShippingUsd": 14.99, "freeShippingEnabled": false, "freeShippingThresholdUsd": 50.00, "catalogCacheTtlMinutes": 15, "claimPageUrl": "https://your-store.com/pages/claim-your-order", "registryCategories": "[\"commerce\",\"food-and-beverage\",\"coffee\"]", "registryDescription": "Premium coffee beans available for AI agent purchases via Lightning L402 payments." }' ``` Lightning Enable exchanges your Client ID and Client Secret with Shopify for a short-lived access token (valid ~24 hours), then reuses that token for Admin API calls until it needs to be refreshed. You don't need to rotate or refresh tokens manually — Lightning Enable handles that automatically and transparently. ### Configuration Options | Field | Required | Description | |-------|----------|-------------| | `slug` | Yes | URL-safe identifier for your store (lowercase, hyphens OK). Used in endpoint URLs: `/api/shopify/{slug}/catalog` | | `shopifyDomain` | Yes | Your Shopify store domain (e.g., `your-store.com` or `your-store.myshopify.com`). On save, Lightning Enable calls Shopify's `shop.json` and auto-corrects this to your customer-facing primary domain — your customers see `your-store.com`, your canonical `*.myshopify.com` handle is captured separately and managed for you. You can refresh both at any time from the dashboard. | | `shopifyClientId` | Yes | Shopify app **Client ID** from Step 1 | | `adminApiAccessToken` | Yes | Shopify app **Client Secret** (starts with `shpss_`) from Step 1. If you use a legacy custom app's Admin API access token (`shpat_`) instead, omit `shopifyClientId` and put the token here. | | `domesticShippingUsd` | No | US flat-rate shipping (default: $5.99) | | `internationalShippingUsd` | No | International flat-rate shipping (default: $14.99) | | `freeShippingEnabled` | No | Enable free shipping above threshold (default: false) | | `freeShippingThresholdUsd` | No | Order subtotal for free shipping (default: $50.00) | | `catalogCacheTtlMinutes` | No | How long to cache products (default: 15 min, range: 1-1440) | | `domesticOnly` | No | **Default: `true`** — claims with a non-US shipping address are rejected until you flip this to `false`. Set it explicitly if you ship internationally. | | `claimExpiryDays` | No | Claim window after checkout, in days (default: 30, range: 1-365). Buyers must submit shipping details within this window. | | `allowedProductTypes` | No | Comma-separated list of Shopify `product_type` values to expose in the L402 catalog (case-insensitive). Empty = all active products. | | `claimPageUrl` | Recommended | URL of your claim page (see Step 7). When set, post-payment responses and the buyer's recovery email carry the claim token inside this URL (`?token=...`). When not set, the paid checkout retry returns the raw `claimToken` instead, and a buyer who gave an email at checkout is emailed the claim code rather than a link, but buyers can't use a browser form. You can add it after creating the integration. | | `listInRegistry` | No | List in L402 API registry for agent discovery (default: true) | | `registryCategories` | No | JSON array of categories, e.g. `["commerce","food-and-beverage"]` | | `registryDescription` | No | Description for registry keyword search | ### Choosing a Slug Your slug determines the public URL for your store's endpoints: ``` https://api.lightningenable.com/api/shopify/{slug}/catalog https://api.lightningenable.com/api/shopify/{slug}/checkout https://api.lightningenable.com/api/shopify/{slug}/claim ``` Choose something short and memorable. It must be: - Lowercase letters, numbers, and hyphens only - Unique across all Lightning Enable merchants - Examples: `my-store`, `acme-goods`, `coffee-co` ## Step 6: Verify Your Setup ### Test the Catalog ```bash curl https://api.lightningenable.com/api/shopify/your-store-name/catalog ``` You should see your products with variants, prices, and shipping rules. No authentication is required for this endpoint. ### Test a Checkout ```bash # Use a real variant ID from the catalog response # X-Buyer-Location is REQUIRED for tax calculation — pass the buyer's country-state-zip curl -X POST https://api.lightningenable.com/api/shopify/your-store-name/checkout \ -H "Content-Type: application/json" \ -H "X-Buyer-Location: US-FL-34787" \ -d '{ "items": [ {"variantId": 12345678901234, "quantity": 1} ] }' ``` You should receive an HTTP 402 response with: - A Lightning invoice (BOLT11 string) - A macaroon (base64) - A payment hash - Order details with amounts in both USD and sats - Tax amount (`taxUsd`; `taxNote` is always `null`) :::note No claim token in the 402 response The claim token is deliberately **not** in the checkout response — exposing it pre-payment would let an observer race the buyer to claim the order. It's returned after you prove payment (step 2 below). ::: ### Test a Full Purchase To test the complete flow, use the [Lightning Enable MCP server](/products/agentic-commerce/mcp-quickstart): 1. Use the `pay_invoice` tool to pay the invoice from the checkout response 2. **Retry the same checkout endpoint** with your payment proof to verify payment and get the claim URL. Re-send the **same body** you used for the first checkout (the one from "Test a Checkout" above): ```bash curl -X POST https://api.lightningenable.com/api/shopify/your-store-name/checkout \ -H "Content-Type: application/json" \ -H "Authorization: L402 MACAROON_BASE64:PREIMAGE_HEX" \ -d '{"items": [{"variantId": 12345678901234, "quantity": 1}]}' ``` The body must be valid (an empty `{"items": []}` returns `400`), but its content doesn't affect the retry: the payment credential identifies the paid order. The `200` response includes `claimPageUrl` — your claim page (from Step 7) with the claim token appended as `?token=SC-xxxxxxxx`. If you haven't set a claim page yet, `claimPageUrl` is `null` and the response carries the token as `claimToken` instead. 3. Claim the order — open `claimPageUrl` in a browser and submit the shipping form, or call the API directly with the token from the URL or `claimToken` (the order is already marked paid, so the token alone is sufficient): ```bash curl -X POST https://api.lightningenable.com/api/shopify/your-store-name/claim \ -H "Content-Type: application/json" \ -d '{ "claimToken": "SC-xxxxxxxx", "email": "customer@example.com", "shippingAddress": { "firstName": "Jane", "lastName": "Doe", "address1": "123 Main St", "city": "Austin", "province": "TX", "zip": "78701", "country": "US" } }' ``` 4. Check your Shopify admin — a new order should appear, marked as **paid** 5. Verify order tracking works — the claim token remains valid for status lookups for **90 days after claiming**: ```bash curl "https://api.lightningenable.com/api/shopify/your-store-name/orders/ORDER_ID?claimToken=SC-xxxxxxxx" ``` ## Step 7: Add the Claim Page and `llms.txt` to Your Store After an AI agent pays the Lightning invoice, a human still needs to submit a shipping address. Lightning Enable provides a Shopify section + page template (compatible with Dawn and any OS 2.0 theme) and an `llms.txt` template to run this flow on your own domain so the buyer experience stays within your brand. The templates are provided when you start your integration: - **`le-claim.liquid`** — a configurable section that renders the shipping-details form after payment. Brand colors, copy, card layout, and typography are all set in the theme editor — no code edits per merchant. - **`page.claim.json`** — the page template that wires the section into a Shopify page. - **`llms-txt.liquid`** — a machine-readable page that tells AI agents how to browse and purchase from your store using L402. ### Install the claim section + page template 1. Shopify admin → **Online Store** → **Themes** → **Edit code** 2. Under **Sections**, click **Add a new section** → name it `le-claim` → choose **Liquid** → paste the contents of `sections/le-claim.liquid` 3. Under **Templates**, click **Add a new template** → type `page` → name it `claim` → choose **JSON** → paste the contents of `templates/page.claim.json` 4. Shopify admin → **Online Store** → **Pages** → **Add page**, title it `Claim Your Order`, leave the body empty, and under **Theme template** select `claim` → save and copy the page URL 5. In your [Lightning Enable dashboard](https://api.lightningenable.com/dashboard), set **Claim Page URL** to the page URL (e.g., `https://your-store.com/pages/claim-your-order`) 6. Open the page in the **theme editor** and click the **Lightning Enable Claim** section. Set the required **slug** (your Lightning Enable store slug from Step 5) and adjust brand colors, copy, and typography to match your theme. The default white card works on any background — leave it on unless your page already provides a contrasting container. ### Install the `llms.txt` template 1. Shopify admin → **Online Store** → **Themes** → **Edit code** 2. Under **Templates**, click **Add a new template** → type `page` → name it `llms-txt` 3. Paste the contents of `llms-txt.liquid` into the new template 4. In Shopify admin → **Online Store** → **Pages** → **Add page**, title it `llms`, leave the body empty, and under **Theme template** select `llms-txt` 5. Verify at `https://your-store.com/pages/llms` — you should see plain text (no site header or footer) :::tip Make `llms.txt` discoverable at your domain root AI agents typically look for `llms.txt` at `https://your-store.com/llms.txt`. Because Shopify serves custom pages under `/pages/`, add a URL redirect in Shopify admin (**Online Store** → **Navigation** → **URL Redirects**) from `/llms.txt` to `/pages/llms` so agents can find it at the canonical path. ::: ## Step 8: Go Live Once testing is complete, your L402 store endpoints are live and ready for AI agents. Your store is **automatically listed** in the L402 API registry, which means AI agents can find it using the `discover_api` MCP tool: ``` Agent: discover_api(query="coffee") → Your store appears in results with description, categories, and manifest URL ``` You can also share your catalog URL directly: ``` https://api.lightningenable.com/api/shopify/your-store-name/catalog ``` To manage your registry listing (categories, description, or opt out), see the [Agent Discovery & Registry](/products/shopify-commerce/agent-discovery) guide. ## Managing Your Integration ### Update Settings ```bash curl -X PUT https://api.lightningenable.com/api/merchant/shopify \ -H "X-API-Key: YOUR_LIGHTNING_ENABLE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "domesticShippingUsd": 6.99, "freeShippingEnabled": true, "freeShippingThresholdUsd": 75.00 }' ``` Only include fields you want to change — all others remain unchanged. ### Refresh the Catalog Cache After updating products or prices in Shopify, force a cache refresh: ```bash curl -X POST https://api.lightningenable.com/api/merchant/shopify/invalidate-cache \ -H "X-API-Key: YOUR_LIGHTNING_ENABLE_API_KEY" ``` Or wait for the cache TTL to expire naturally (default: 15 minutes). ### View Orders ```bash # All orders curl "https://api.lightningenable.com/api/merchant/shopify/orders" \ -H "X-API-Key: YOUR_LIGHTNING_ENABLE_API_KEY" # Filter by status curl "https://api.lightningenable.com/api/merchant/shopify/orders?status=PaidWithDetails" \ -H "X-API-Key: YOUR_LIGHTNING_ENABLE_API_KEY" # Paginate curl "https://api.lightningenable.com/api/merchant/shopify/orders?page=2&pageSize=10" \ -H "X-API-Key: YOUR_LIGHTNING_ENABLE_API_KEY" ``` ### Pause the Integration To temporarily disable L402 purchases without losing your configuration: ```bash curl -X PUT https://api.lightningenable.com/api/merchant/shopify \ -H "X-API-Key: YOUR_LIGHTNING_ENABLE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"isActive": false}' ``` Set `isActive` back to `true` to re-enable. ## Troubleshooting ### `Invalid API key or access token` from Shopify - Double-check that the **Client ID** and **Client Secret** pasted into Lightning Enable match what's shown in your Shopify app's configuration, with no extra whitespace. - Confirm the app is still **installed** on your store (Settings → Apps and sales channels → Develop apps → your app). Uninstalling the app revokes its credentials immediately. - If you regenerated the Client Secret, update it in your Lightning Enable dashboard — the previous secret stops working immediately. - If you updated the app's Admin API scopes after install, Shopify requires the app to be re-installed for the new scopes to take effect. Open the app in the Dev Dashboard and click **Install** (or **Update**) to re-apply. ### Catalog endpoint returns an empty list - Confirm your products are **Active**. Publishing to the **Online Store** sales channel is only required for Shopify's public storefront endpoints, not for the Admin API catalog fetch Lightning Enable uses. - Open `https://your-store.com/products.json` directly only if you want to verify public storefront visibility or suspect Lightning Enable is falling back to Shopify's public endpoint. If it returns an empty list, Shopify isn't serving the products publicly yet, but the Admin API catalog fetch can still work. - If you set `allowedProductTypes` on your integration, the values must match a product's Shopify `product_type` field, but letter casing does not matter. - The catalog is cached for 15 minutes by default — trigger a refresh by calling the `invalidate-cache` endpoint shown above in **Refresh the Catalog Cache**. ### Checkout returns a 400 about location Tax calculation needs to know where the buyer is. The agent must pass an `X-Buyer-Location` header on the checkout request (format `US-FL-34787`) — this header is **required** and there is no fallback. If it is missing or blank, checkout returns `400`. ### Order appears as unpaid or with no shipping info in Shopify admin - An order marked `PaidAwaitingDetails` in Lightning Enable means the Lightning payment succeeded but the buyer hasn't submitted the claim form yet. The default claim window is 30 days. - If the buyer never claims, the order does not post to your Shopify store at all — the agent paid, but no fulfillment request was ever made. Refunds for these cases are handled through your payment provider (e.g., Strike dashboard) as outgoing Lightning payments. ### Lightning invoice not being created at checkout - Verify your Strike API key is active in the Strike dashboard and has the [required scopes](/strike-setup/account-setup#api-key-scopes). - Re-save the key in your Lightning Enable dashboard to confirm it's stored correctly. ## Next Steps - [API Reference](/products/shopify-commerce/api-reference) — Full endpoint documentation with request/response schemas - [MCP Quickstart](/products/agentic-commerce/mcp-quickstart) — Connect an AI agent that can buy from your store - [Strike Setup](/strike-setup/account-setup) — Configure Strike as your payment provider ============================================================================== # checkout.js Source: https://docs.lightningenable.com/products/standalone/checkout-js ============================================================================== # checkout.js `checkout.js` (v2.1.0) is a small browser helper that connects your payment buttons to Lightning Enable's hosted checkout page. It handles the redirect UX — loading states, error callbacks, and sending the customer to the checkout URL your backend creates. :::info Security model - **Prices are never passed from the client** — that would let anyone edit the amount. - **Your API key never reaches the browser.** Your backend creates the payment with `X-API-Key`; the script only receives a checkout URL. - The script does not create invoices, render QR codes, or poll payment status — the hosted checkout page does that. ::: ## Architecture ``` Customer clicks button → checkout.js calls YOUR backend endpoint → your backend calls POST /api/payments (with your API key) → your backend returns { "checkoutUrl": "https://api.lightningenable.com/pay/" } → checkout.js redirects the customer to the hosted checkout page ``` Lightning Enable does not hold funds; your payment provider (Strike or OpenNode) or your own wallet over NWC facilitates custody and settlement. ## Quick Start ### 1. Include the script ```html ``` ### 2. Create a checkout endpoint on your backend Your endpoint creates the payment and returns the checkout URL. `POST /api/payments` gives you that URL ready-made in `payUrl` — pass it straight through. :::caution The checkout URL is a secret The hosted checkout page lives at `https://api.lightningenable.com/pay/{paymentToken}`, where `paymentToken` is a random per-invoice value returned only in the create-payment response. It is what authorizes the page — anyone holding it can view that invoice's amount, description and Lightning invoice — so treat it like a payment link: send it to the buyer, don't log it, don't put it in a page a search engine can crawl. It cannot be derived from `invoiceId`. Lost it? Re-read `payUrl` from `GET /api/payments/{invoiceId}` with your API key. ::: ```javascript // Node/Express example app.post('/api/create-checkout', async (req, res) => { const response = await fetch('https://api.lightningenable.com/api/payments', { method: 'POST', headers: { 'X-API-Key': process.env.LIGHTNING_ENABLE_API_KEY, // server-side only 'Content-Type': 'application/json' }, body: JSON.stringify({ orderId: 'order-' + Date.now(), amount: 25.00, // price from YOUR catalog — never from the client currency: 'USD', description: 'Premium Widget' }) }); if (!response.ok) { return res.status(502).json({ error: 'Could not create checkout' }); } const payment = await response.json(); res.json({ checkoutUrl: payment.payUrl }); }); ``` ### 3. Add a payment button **Option A — dynamic session creation** (SPAs / dynamic sites). The button calls your endpoint, then redirects: ```html ``` **Option B — pre-created checkout URL** (static sites). Create the session ahead of time and link straight to it: ```html Pay with Lightning ``` Buttons are wired up automatically when the DOM is ready — no initialization call required. ## Configuration `LightningCheckout.init()` is optional. Call it only to customize error handling or the redirect: ```javascript LightningCheckout.init({ onError: function (error) { // error.message describes what failed (endpoint unreachable, missing checkoutUrl, ...) showToast('Payment failed to start: ' + error.message); }, onRedirect: function (url) { // Default is window.location.href = url. Override to open in a new tab, log, etc. window.open(url, '_blank'); } }); ``` | Option | Type | Default | Description | |--------|------|---------|-------------| | `onError` | `function(error)` | `console.error` | Called when session creation fails or a button is misconfigured | | `onRedirect` | `function(url)` | `window.location.href = url` | Called with the checkout URL to navigate to | ## Button attributes | Attribute | Applies to | Description | |-----------|-----------|-------------| | `data-checkout-endpoint` | Option A | Your backend endpoint that returns `{ "checkoutUrl": "..." }` | | `data-checkout-method` | Option A | HTTP method for the endpoint call (default `POST`) | | `data-checkout-url` | Option B | Pre-created checkout URL to redirect to | | any other `data-*` | Option A | Sent to your endpoint as the JSON request body — e.g. `data-product-id="42"` posts `{ "productId": "42" }` | While a session is being created the button is disabled and gets the `lightning-checkout-loading` CSS class (dimmed, `cursor: wait`) — style it further if you like. ## JavaScript API ```javascript LightningCheckout.init(options); // optional — configure callbacks LightningCheckout.redirectToCheckout(checkoutUrl); // programmatic redirect LightningCheckout.version; // "2.1.0" ``` Programmatic flow example: ```javascript async function buy(productId) { const res = await fetch('/api/create-checkout', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ productId }) }); const { checkoutUrl } = await res.json(); LightningCheckout.redirectToCheckout(checkoutUrl); } ``` ## Confirming payment The hosted checkout page shows the invoice and updates when payment lands. To fulfill the order, use one of the server-side paths: - **Webhooks (recommended):** configure your callback URL and verify the `X-LightningEnable-Signature` header — see [Webhooks](/products/standalone/webhooks). - **Polling from your backend:** `GET /api/payments/{invoiceId}` with your API key. - **Browser-side UX only:** the public `GET /api/payments/{invoiceId}/status` endpoint returns `{ "status": "..." }` without an API key. ## Availability checkout.js and the hosted checkout page work on every plan, including the Free Producer Sandbox. See [Plans](/products/product-overview). ## Next Steps - [Integration Guide](/products/standalone/integration) — the full REST API flow - [Webhooks](/products/standalone/webhooks) — payment notifications ============================================================================== # Integration Guide Source: https://docs.lightningenable.com/products/standalone/integration ============================================================================== # Integration Guide This guide covers the Core Payments API end to end: create a payment, show it to the buyer, confirm it was paid, and fulfill the order. It works on every plan. ## Authentication Send your merchant API key in the `X-API-Key` header. Keep it server-side; never put it in browser code. ```bash curl https://api.lightningenable.com/api/payments/1042 \ -H "X-API-Key: $LIGHTNING_ENABLE_API_KEY" ``` ## Creating Payments ```javascript const response = await fetch('https://api.lightningenable.com/api/payments', { method: 'POST', headers: { 'X-API-Key': process.env.LIGHTNING_ENABLE_API_KEY, 'Content-Type': 'application/json', 'X-Idempotency-Key': 'order-12345-attempt-1' // optional: safe retries }, body: JSON.stringify({ orderId: 'ORDER-12345', amount: 99.99, currency: 'USD', description: 'Premium Widget', successUrl: 'https://yourapp.com/success', metadata: { customerId: 'cust_123' } }) }); const payment = await response.json(); // 201 Created ``` ### Request Fields | Field | Type | Required | Description | |-------|------|----------|-------------| | `orderId` | string | Yes | Your order reference, 1 to 100 characters | | `amount` | decimal | Yes | Amount in `currency` | | `currency` | string | No | Three-letter code, default `USD`. Strike: USD, EUR, GBP, BTC. NWC: BTC only | | `description` | string | No | Up to 500 characters; trimmed to the provider's invoice limit | | `customerEmail` | string | No | Customer email | | `customerName` | string | No | Customer name | | `successUrl` | string | No | Where hosted checkout sends the buyer after payment | | `metadata` | object | No | String key-value pairs, returned in the webhook as a JSON string | | `expiresInSeconds` | integer | No | Invoice lifetime, 60 to 86400. Omit for the provider default (about 24 hours on Strike). Out of range returns `400 invalid_expiry` | Send `X-Idempotency-Key` (up to 256 characters) to make retries safe: repeating a request with the same key returns the original payment instead of creating a second invoice. ### Response Fields | Field | Type | Description | |-------|------|-------------| | `invoiceId` | string | Lightning Enable invoice ID (numeric string). Use it for status calls | | `status` | string | `unpaid`, `processing`, `paid`, `expired`, or `refunded` | | `amount` / `currency` | | As requested | | `lightningInvoice` | string | BOLT11 invoice to pay | | `paymentHash` | string | The invoice's payment hash | | `onchainAddress` | string | Bitcoin address, when the provider offers one (Strike, OpenNode) | | `payUrl` | string | Hosted checkout link, `https://api.lightningenable.com/pay/{token}` | | `providerChargeId` | string | The provider's ID for this invoice (`openNodeChargeId` carries the same value for older clients) | | `createdAt` / `expiresAt` | datetime | `expiresAt` is the BOLT11 invoice's own deadline | ## Showing the Payment ### Hosted checkout (simplest) Redirect the buyer to `payUrl`. The page shows the QR code, Lightning and on-chain options, a countdown, and redirects to `successUrl` once paid. ```javascript window.location.href = payment.payUrl; ``` :::caution Treat `payUrl` as a payment link Anyone holding it can view that invoice. Send it to the buyer, but don't log it or publish it where a crawler can find it. It cannot be rebuilt from `invoiceId`; if you lose it, read it again from `GET /api/payments/{invoiceId}`. ::: For a ready-made button, see [checkout.js](/products/standalone/checkout-js). ### Your own UI Render `lightningInvoice` as a QR code with any QR library (prefix it with `lightning:` for wallet deep links), show a copy button, and count down to `expiresAt`. ## Confirming Payment ### Webhooks (recommended) Set a callback URL and secret with `PUT /api/merchant/webhook-url`, verify the `X-LightningEnable-Signature` header, and fulfill on `status === 'paid'`. See [Webhooks](/products/standalone/webhooks). ### Polling `GET /api/payments/{invoiceId}` (with your API key) is the authoritative status. `POST /api/payments/{invoiceId}/sync` re-checks the provider immediately. Poll no more than every few seconds: ```javascript async function waitForPayment(invoiceId) { for (;;) { const res = await fetch(`https://api.lightningenable.com/api/payments/${invoiceId}`, { headers: { 'X-API-Key': process.env.LIGHTNING_ENABLE_API_KEY } }); const payment = await res.json(); if (payment.status === 'paid' || payment.status === 'expired') return payment; await new Promise(r => setTimeout(r, 3000)); } } ``` The unauthenticated `GET /api/payments/{invoiceId}/status` returns only `{ "status": "..." }`. Use it for browser-side UX, never to decide fulfillment. On the NWC lane, payment is detected by polling your wallet once a minute, so allow up to 60 seconds. ## Error Handling Most error bodies carry `error` and `message`; a request that fails field validation returns a problem-details body naming the field. Common cases on `POST /api/payments`: | Status | `error` | What to do | |--------|---------|------------| | `400` | `invalid_expiry`, `payment_provider_not_configured`, or field validation | Fix the request, or save a provider credential | | `401` | `API key required` / `Invalid API key` | Check `X-API-Key` | | `403` | `Subscription not active` and related | Route on `action_required` | | `429` | | Rate limited; wait and retry with the same `X-Idempotency-Key` | | `502` | `payment_provider_rejected` | The provider refused the invoice; `message` gives its reason. Not retryable as is | | `502` | `payment_provider_unavailable` | The provider is down; retry later | | `503` | `payment_provider_busy` | NWC wallet busy; retry after `Retry-After` seconds | See [Errors](/api-reference/errors) for the full list. ## Python Example ```python import os import requests BASE_URL = 'https://api.lightningenable.com' HEADERS = {'X-API-Key': os.environ['LIGHTNING_ENABLE_API_KEY']} def create_payment(order_id, amount, currency='USD', **fields): response = requests.post( f'{BASE_URL}/api/payments', headers={**HEADERS, 'X-Idempotency-Key': f'{order_id}-1'}, json={'orderId': order_id, 'amount': amount, 'currency': currency, **fields}, timeout=30, ) response.raise_for_status() return response.json() def get_payment(invoice_id): response = requests.get(f'{BASE_URL}/api/payments/{invoice_id}', headers=HEADERS, timeout=30) response.raise_for_status() return response.json() ``` ## Best Practices - Keep the API key server-side and verify every webhook signature. - Store `invoiceId` against your order, and make fulfillment idempotent: webhooks can repeat. - Reconcile pending orders with `GET /api/payments/{invoiceId}` on a schedule, so a missed webhook never strands an order. - Treat `expired` as final; create a new payment for a retry. ## Next Steps - [Webhooks](/products/standalone/webhooks): real-time notifications - [Payments API Reference](/api-reference/payments): every endpoint ============================================================================== # Core Payments API (REST) Source: https://docs.lightningenable.com/products/standalone/overview ============================================================================== # Core Payments API (REST) The Core Payments API is the REST surface for taking a one-off Lightning payment: create an invoice, send the buyer to a hosted checkout page, and receive a signed webhook when it is paid. It is not a separate plan. Every Lightning Enable account can use it, including the [Free Producer Sandbox](/getting-started/activate-with-lightning); see [Plans](/products/product-overview) for what each plan adds. Lightning Enable does not hold funds. Your payment provider (Strike or OpenNode) or your own wallet over [Nostr Wallet Connect](/nwc-setup/account-setup) facilitates custody and settlement. ## What you need 1. A Lightning Enable merchant API key (sent as `X-API-Key`). 2. A payment provider credential saved on your account: [Strike](/strike-setup/account-setup) (default), [NWC](/nwc-setup/account-setup), or [OpenNode](/opennode-setup/account-setup) (legacy). ## The flow ```bash curl -X POST https://api.lightningenable.com/api/payments \ -H "X-API-Key: your-api-key" \ -H "Content-Type: application/json" \ -d '{ "orderId": "ORDER-12345", "amount": 49.99, "currency": "USD", "description": "Premium Widget" }' ``` 1. `POST /api/payments` returns `201` with `invoiceId`, the BOLT11 `lightningInvoice`, `expiresAt`, and `payUrl`. 2. Send the buyer to `payUrl` (the hosted checkout page at `/pay/{token}`), or render `lightningInvoice` as a QR code yourself. 3. Lightning Enable posts a signed webhook to your callback URL when the status changes. `GET /api/payments/{invoiceId}` is the authoritative status if a webhook is missed. ## Currencies Strike invoices can be priced in USD, EUR, GBP, or BTC, and OpenNode also accepts fiat-priced charges. The NWC lane prices in BTC only, because a wallet has no exchange rate. ## Next steps - [Integration Guide](/products/standalone/integration): request and response fields, errors, and code samples - [checkout.js](/products/standalone/checkout-js): a payment button that redirects to hosted checkout - [Webhooks](/products/standalone/webhooks): payload shapes and signature verification - [Agentic Commerce](/products/agentic-commerce/overview): charge per request with L402 instead of per order ============================================================================== # Webhooks Source: https://docs.lightningenable.com/products/standalone/webhooks ============================================================================== # Webhooks When a payment's status changes, Lightning Enable posts a signed JSON payload to your callback URL. Use webhooks to fulfill orders, and keep `GET /api/payments/{invoiceId}` as the authoritative fallback. ## Setup ### 1. Set your callback URL and secret Generate a secret (for example, `openssl rand -hex 32`) and keep a copy. Lightning Enable never returns it, and your account's initial secret is not readable, so set your own: ```bash curl -X PUT https://api.lightningenable.com/api/merchant/webhook-url \ -H "X-API-Key: your-merchant-api-key" \ -H "Content-Type: application/json" \ -d '{ "webhookUrl": "https://yourapp.com/webhooks/lightning", "webhookSecret": "your-webhook-secret" }' ``` Or send only `webhookUrl` and let Lightning Enable generate the secret: rotate it, and store the value the response returns. The merchant API returns it only in that response; to get a value again, rotate again. ```bash curl -X POST https://api.lightningenable.com/api/merchant/webhook-secret/rotate \ -H "X-API-Key: your-merchant-api-key" ``` See [Rotate Webhook Secret](/api-reference/merchant-settings#rotate-webhook-secret). Your endpoint must accept `POST`, be reachable over HTTPS, and return a `2xx` within 10 seconds. ### 2. Verify the signature Every delivery carries an `X-LightningEnable-Signature` header: ``` X-LightningEnable-Signature: t=1735689600,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8f9 ``` `v1` is the lowercase hex HMAC-SHA256 of `{t}.{raw_body}`, keyed with your webhook secret. To verify: 1. Parse `t` and `v1` from the header; reject a malformed header. 2. Reject a `t` more than 5 minutes old (replay protection). 3. Compute the HMAC over the **raw** request body, before any JSON parsing. 4. Compare with `v1` in constant time. Copy a tested verifier from [Verifying Webhooks](/api-reference/webhooks#verifying-webhooks) ([Node.js](/api-reference/webhooks#nodejs--javascript-verification), [C#](/api-reference/webhooks#c-verification), [Python](/api-reference/webhooks#python-verification)). ### 3. Handle the event ```javascript // Express: express.raw() keeps the exact bytes the signature covers app.post('/webhooks/lightning', express.raw({ type: 'application/json' }), async (req, res) => { const payload = req.body.toString('utf8'); const header = req.headers['x-lightningenable-signature']; if (!header || !verifyWebhookSignature(payload, header, process.env.WEBHOOK_SECRET)) { return res.status(401).send('Invalid signature'); } res.status(200).send('OK'); // acknowledge first, then do the slow work const event = JSON.parse(payload); if (event.event) return; // an L402 producer event; see the API reference if (event.status === 'paid') { await fulfillOnce(event.orderId, event.invoiceId); // idempotent } }); ``` ## Payload One flat JSON object per event, with no envelope. The provider field differs by lane: ```json { "invoiceId": "1042", "orderId": "ORDER-12345", "status": "paid", "amount": 25.00, "currency": "USD", "providerChargeId": "8f6c3f5e-1c2d-...", "provider": "strike", "paidAt": "2026-07-03T12:03:45Z", "metadata": "{\"customerId\":\"cust_123\"}" } ``` | Field | Description | |-------|-------------| | `invoiceId` | Lightning Enable invoice ID (numeric string) | | `orderId` | Your order ID | | `status` | Route on this field (see below) | | `amount` / `currency` | As created | | `providerChargeId` + `provider` | Strike (`"strike"`) and NWC (`"nwc"`; the charge ID is the payment hash) | | `openNodeChargeId` | OpenNode merchants, in place of the two fields above | | `paidAt` | When Lightning Enable processed the event (UTC) | | `metadata` | The metadata you supplied, as a **JSON string** (parse it), or `null` | If you sell with L402, an `l402.challenge.paid` event with an `event` field and no `status` arrives on the same URL. Check for `event` first. See [L402 producer events](/api-reference/webhooks#l402-producer-events). ### Statuses | Status | Meaning | Action | |--------|---------|--------| | `paid` | Payment confirmed | Fulfill the order | | `processing` | Payment seen, not final (includes OpenNode underpayments) | Wait | | `expired` | Invoice expired unpaid | Release the order or create a new payment | | `refunded` | Payment refunded | Update records | Refunds themselves send no webhook; poll `GET /api/refunds/{refundId}`. ## Delivery and Retries - The first attempt fires as soon as Lightning Enable sees the status change. On the NWC lane that is within a minute, because the wallet is polled. - A non-`2xx` response or a timeout is retried with exponential backoff: 30s, 60s, 120s, 240s, 480s. Every attempt sends identical payload bytes with a freshly timestamped signature, so dedupe on `invoiceId` + `status`, never on the signature header. - After the last retry the event is marked failed. Reconcile pending orders with `GET /api/payments/{invoiceId}`, or `POST /api/payments/{invoiceId}/sync` to re-check the provider. ## Local Development Expose your local endpoint with a tunnel such as `ngrok http 3000`, then set the tunnel URL with `PUT /api/merchant/webhook-url`. Switch it back to your production URL before you go live. ## Troubleshooting **Nothing arrives.** Confirm the URL is public HTTPS and that a firewall isn't blocking it. Then check the payment's status with `GET /api/payments/{invoiceId}`: if it is still `unpaid`, there was nothing to send yet. **Signature check fails.** 1. Hash the raw body, not re-serialized JSON. 2. Sign `{t}.{body}`, not the body alone. 3. Use the `webhookSecret` you set, not an API key. 4. Read the header `X-LightningEnable-Signature`, and compare lowercase hex. **Duplicates.** Expected. Make fulfillment a no-op for an order you have already fulfilled. ## Next Steps - [Webhooks API Reference](/api-reference/webhooks): full reference and verifiers - [Errors](/api-reference/errors): error codes ============================================================================== # Subscription & Plan Enforcement Source: https://docs.lightningenable.com/products/subscription-management ============================================================================== # Subscription & Plan Enforcement Lightning Enable enforces active subscriptions and plan-specific feature access on every API request. This page explains how plan tiers work, what happens when a subscription lapses, and how feature gating controls access to plan-specific functionality. ## Plan Tiers Lightning Enable has three plan tiers. Pricing is based on capabilities, never transaction volume. Platform integrations (Shopify Commerce) are included with any paid plan. | Plan | Tier ID | Price | Stripe subscription | |------|---------|-------|---------------------| | **Free Producer Sandbox** | `free` | $0 | Not required | | **Agentic Commerce** | `individual` | $49/month | Required | | **Agentic Commerce — Business** | `l402` | Contact us | Required | Agentic Commerce includes a 30-day free trial through self-serve checkout; neither paid plan offers annual billing. Free is the floor: an account with no plan tier set resolves to Free, so there is no paid default. Agentic Commerce — Business is **contact-only**: it is not purchasable through `/Checkout`, `/BitcoinCheckout`, `POST /api/stripe/create-checkout-session`, or `POST /api/bitcoin/create-subscription` — a request naming it on any of those surfaces is rejected with a `400`. To subscribe, email support@lightningenable.com. Existing Business subscribers keep everything shown in this page's tables (feature entitlements, limits, renewal) unaffected. ### Retired tier ids As of September 2026, three tiers exist. The `pilot`, `standalone` (also spelled `standaloneapi`), and `standard` (Kentico Commerce) tiers were removed after a production snapshot confirmed no account was on any of them. Their spellings still **resolve**, so an account created before the change and an old checkout link both keep working: | Retired id | Resolves to | |------------|-------------| | `standard`, `kenticocommerce`, `kentico-commerce`, `kentico` | `individual` | | `standalone`, `standaloneapi`, `standalone-api` | `individual` | | `pilot` | `free` | Retired paid ids resolve to Individual rather than Free so an existing subscriber is never silently downgraded. Resolving is not the same as selecting. You cannot start anything new on a retired tier — `POST /api/stripe/create-checkout-session` and `POST /api/bitcoin/create-subscription` reject a retired id with a `400`. The admin endpoint `PUT /api/admin/merchants/{id}` still accepts one and rewrites it to the live tier it maps to; a tier id that is neither live nor retired is rejected with a `400` that names the accepted values. ### Feature Comparison Every plan includes core API access. Higher tiers unlock additional capabilities. | Feature | Free Producer Sandbox | Agentic Commerce | Agentic Commerce — Business | |---------|------|-----------|------------------------| | Full REST API | Yes | Yes | Yes | | Lightning Network payments | Yes | Yes | Yes | | Multi-currency (USD, EUR, GBP, BTC) | Yes | Yes | Yes | | Analytics | No | Yes | Yes | | Priority support | No | Yes | Yes | | Max environments | 1 | 2 | 2 | | Max webhook endpoints | 1 | 5 | 5 | | Platform integrations (Shopify) | No | Yes | Yes | | [Self-hosted Lightning store](/products/run-your-own-store) (store API) | No | Yes | Yes | | L402 protocol (server-side) | **Capped** | **Yes** | **Yes** | | Pay-per-request monetization | **Capped** | **Yes** | **Yes** | | MCP AI agent integration | **Yes** | **Yes** | **Yes** | | White-glove onboarding | No | No | **Yes** | | Custom branding | No | No | No | Free is capped rather than unlimited: 3 L402 endpoints, 200 challenges per month, 1,000 sats maximum per challenge, and 1 proxy configuration. The endpoint cap counts distinct resource paths for the life of the account; the challenge cap resets monthly. ### Checking Your Plan Use the merchant settings endpoint to see your current plan and features: ```bash curl https://api.lightningenable.com/api/merchant/me \ -H "X-API-Key: YOUR_API_KEY" ``` The response includes your `planTier`, `subscriptionStatus`, and a `features` object with all feature flags. --- ## Free Trial Lightning Enable offers a **30-day free trial** on self-serve Agentic Commerce checkout, giving you full API access before your first payment. ### How It Works - **Eligible plan:** Agentic Commerce ($49/mo), via self-serve checkout - **Duration:** 30 days from the date of subscription - **Card required:** A valid payment method must be provided at signup. You will not be charged during the trial period. - **Full access:** Trial merchants have complete API access, identical to a paid subscription. The `subscriptionStatus` will show `trialing`. - **Auto-converts:** At the end of the 30-day trial, the subscription automatically converts to a paid plan. Your card on file will be charged at the plan's regular rate. - **Cancel anytime:** You can cancel before the trial ends to avoid being charged. Use the Stripe customer portal to manage your subscription. Agentic Commerce — Business is contact-only and does not go through this self-serve trial flow. Email support@lightningenable.com and any trial terms will be arranged directly. ### Abuse Prevention To maintain fair access, Lightning Enable enforces **one free trial per email address**. If a customer has previously used a trial (on any plan), subsequent subscriptions will skip the trial period and begin billing immediately. ### Trial Eligibility by Plan | Plan | Trial Eligible | |------|---------------| | Free Producer Sandbox ($0) | No -- Free is the floor, not a trial | | Agentic Commerce ($49/mo) | Yes, via self-serve checkout | | Agentic Commerce -- Business (contact us) | Arranged directly on contact, not via self-serve checkout | --- ## Subscription Lifecycle ### Valid Subscription States The subscription enforcement middleware checks every authenticated API request. Only two statuses grant access: | Status | Meaning | API Access | |--------|---------|------------| | `active` | Subscription is current and paid | Allowed | | `trialing` | In free trial period | Allowed | | `past_due` | Payment failed, awaiting retry | **Blocked** | | `canceled` | Subscription was canceled | **Blocked** | | `unpaid` | Payment not received | **Blocked** | | `incomplete` | Initial payment not completed | **Blocked** | | `incomplete_expired` | Initial payment window expired | **Blocked** | ### Subscription Validation Flow The middleware performs these checks in order for every authenticated request: 1. **Path exemption** -- Certain paths skip subscription checks entirely (Stripe endpoints, webhooks, the Lightning renew endpoint, health checks, Swagger). 2. **Account active check** -- If the merchant account is deactivated (`isActive = false`), the request is immediately blocked with a `403`. 3. **Free carve-out** -- An account whose tier resolves to `free` skips the subscription checks entirely, because Free requires no Stripe subscription. Capacity is enforced elsewhere, by the Free caps on the L402 and proxy endpoints. Two guards narrow this carve-out (see [Free carve-out guards](#free-carve-out-guards) below). 4. **L402 fast-lane trial carve-out** -- An account created by the L402 Fast Lane also skips the subscription checks for the length of its trial (see [L402 fast-lane trial](#l402-fast-lane-trial) below). 5. **Legacy Bitcoin checkout carve-out** -- An account paid through `/BitcoinCheckout` skips the Stripe checks while `bitcoinSubscriptionExpiresAt` is in the future (see [Legacy Bitcoin checkout](#legacy-bitcoin-checkout) below). 6. **L402 sats subscription carve-out** -- An account paid in sats over L402 skips the Stripe checks while `currentPeriodEnd` is in the future. Once it passes, the request gets a `403` with `action_required: "renew_subscription"` and the renew endpoint (see [L402 sats subscription](#l402-sats-subscription) below). 7. **Stripe subscription required** -- Every account that took no carve-out must have a valid `StripeSubscriptionId`. Accounts without one receive a `403` with `action_required: "subscribe"`. 8. **Subscription status check** -- The status must be `active` or `trialing`. Any other status returns a `403` with a status-specific message. 9. **Billing period validation** -- If `CurrentPeriodEnd` is set and has passed, the request is blocked even if the status field still shows `active`. This catches expired subscriptions before the Stripe webhook updates the status. 10. **Feature gating** -- Plan-specific features are checked against the requested endpoint (see [Feature Gating](#feature-gating) below). This step runs on **every** path that reached it, carve-outs included, so a Free account cannot reach a paid-only endpoint just because the subscription check was skipped. ``` Request │ ├─ Exempt path? ──── Yes ──→ Allow │ ├─ No MerchantId? ── Yes ──→ Allow (unauthenticated) │ ├─ Account inactive? ────── → 403 "Account inactive" │ ├─ Tier resolves to free │ (and may take a carve-out)? ─ Yes ──→ Feature gate ──→ Allow │ ├─ L402 fast-lane trial, │ still within CurrentPeriodEnd? ─ Yes ─→ Feature gate ──→ Allow │ ├─ Legacy Bitcoin checkout, │ still within bitcoinSubscriptionExpiresAt? ─ Yes ─→ Feature gate ──→ Allow │ ├─ L402 sats subscription │ (paymentMethod l402, no Stripe sub)? │ ├─ CurrentPeriodEnd in future ──→ Feature gate ──→ Allow │ └─ Otherwise ──→ 403 "Subscription period expired" (renew_subscription) │ ├─ No Stripe sub? ─────────→ 403 "Subscription required" │ ├─ Status not active/trialing? → 403 "Subscription not active" │ ├─ CurrentPeriodEnd passed? ──→ 403 "Subscription period expired" │ ├─ Feature not available? ────→ 403 "Feature not available" │ └─ All checks pass ─────────→ Allow (features set in context) ``` ### Free carve-out guards The Free carve-out grants API access with no Stripe subscription, so two states are deliberately excluded from it: - **An unrecognized tier id.** A tier value that is neither live nor retired — an operator typo, say — does **not** take the carve-out. It falls through to the subscription checks, which fail closed. Reading an unrecognized value as Free would hand it permanent unpaid access. - **A blank tier that carries Stripe billing state.** A blank tier normally means "never set", and Free is the right reading. A blank tier on a row that also holds a `stripeCustomerId` or `stripeSubscriptionId` is a data anomaly, not a Free account, so it faces the subscription checks. Otherwise a canceled subscription would regain full access by way of the carve-out. A blank tier with no billing state is the ordinary case and is admitted as Free. ### L402 fast-lane trial An account created through the [L402 Fast Lane](/getting-started/activate-with-lightning) has no Stripe subscription — the 100-sat Lightning payment is the proof of intent — so it needs a carve-out of its own. It applies only when all four of these hold: - the stored tier is exactly `individual` - `subscriptionStatus` is `trialing` - both `stripeSubscriptionId` and `stripeCustomerId` are empty - `currentPeriodEnd` is in the future The first condition matches the **stored** value, not the resolved one. That is deliberate and narrower than it looks: an account stored under a retired id resolves to `individual` everywhere else but does **not** take this carve-out, because the background job that ends the trial filters on the stored string in SQL and could not find it either. Matching wider here than the job can find would turn a bounded trial into permanent unpaid access. The trial ends by way of a background job that downgrades the account to Free once `trialEnd` passes with no Stripe conversion. That job additionally requires the account to be marked as fast-lane-originated and not to have billing deferred, so an account with deferred billing stays on Individual rather than being downgraded. Adding billing mid-trial bills immediately and does not start a second trial. ### L402 sats subscription You can pay for Agentic Commerce in sats instead of by card: one payment buys 30 days, paid over L402 at `POST /api/merchant/subscription/renew`. The price is the `priceSats` that `GET /api/merchant/subscription/renew` quotes. The sats price is configuration and can change; a challenge you already hold is honored at the price it was issued at unless the price has gone up, in which case mint a fresh one. The walkthrough is on [Renew in sats](/getting-started/activate-with-lightning#renew-in-sats); the wire contract is in the [Merchant Settings API](/api-reference/merchant-settings#renew-with-lightning-l402). A credited payment leaves the account in this state: | Field | Value | |-------|-------| | `planTier` | `individual` | | `paymentMethod` | `l402` | | `subscriptionStatus` | `active` | | `stripeSubscriptionId` | empty (a canceled Stripe subscription ID is cleared; the Stripe customer ID is kept) | | `currentPeriodEnd` | the paid-through date | The middleware lets this account through, with feature gating, while `currentPeriodEnd` is in the future. A row that still carries a Stripe subscription ID isn't treated as a sats account whatever its `paymentMethod` says; the Stripe checks decide it. When `currentPeriodEnd` passes, there's no Stripe dunning and no retry: nothing charges the account. Requests get a `403` naming the renew endpoint (see [What happens when a subscription expires](#what-happens-when-a-subscription-expires) below) until a daily job, at 03:30 UTC, moves the account to the Free Producer Sandbox and emails it. From then on the Free carve-out applies and the API key keeps working within the Free limits. The job never deactivates the account, because API-key authentication admits only active accounts and a lapsed account must still reach the renew endpoint to pay. Paying again at any time restores Agentic Commerce, with the 30 days starting from the payment. Lightning Enable does not hold funds; the payment provider facilitates custody and settlement. ### Legacy Bitcoin checkout Accounts paid through the legacy `/BitcoinCheckout` page carry `paymentMethod: "bitcoin"` and no Stripe subscription. The middleware honors them while `bitcoinSubscriptionExpiresAt` is in the future. While that period is still running, the account can also renew in sats; the 30 days start when the Bitcoin period ends. Once `bitcoinSubscriptionExpiresAt` passes, the legacy Bitcoin job deactivates the account, and API-key authentication admits only active accounts, so it can't reach the renew endpoint. Email support@lightningenable.com to restore it. --- ## Subscription Expiration ### What Happens When a Subscription Expires When a subscription expires or is canceled, API requests return `403 Forbidden` with a JSON body describing the issue and what action to take. **Canceled subscription:** ```json { "error": "Subscription not active", "message": "Your subscription has been canceled. Please subscribe again to continue using the service.", "subscription_status": "canceled", "action_required": "renew_subscription" } ``` **Past due payment:** ```json { "error": "Subscription not active", "message": "Your subscription payment is past due. Please update your payment method to continue using the service.", "subscription_status": "past_due", "action_required": "update_payment_method" } ``` **Billing period expired (webhook delay protection):** ```json { "error": "Subscription period expired", "message": "Your subscription billing period has expired. Please renew your subscription to continue using the service.", "subscription_status": "active", "current_period_end": "2026-01-15T00:00:00.0000000Z", "action_required": "renew_subscription" } ``` **Lightning-paid period expired (L402 sats subscription):** ```json { "error": "Subscription period expired", "message": "Your Lightning-paid subscription period has ended. Renew now with an L402 payment, or the account moves to the Free Producer Sandbox within 24 hours.", "subscription_status": "active", "current_period_end": "2026-11-10T14:03:22.0000000Z", "action_required": "renew_subscription", "renew": { "endpoint": "/api/merchant/subscription/renew" } } ``` The `renew` object appears only on this body. When it's present, pay at `renew.endpoint` rather than sending the user to card checkout. :::warning CurrentPeriodEnd Validation Even if the subscription status still reads `active`, the middleware checks whether `CurrentPeriodEnd` has passed. This provides a safety net for cases where Stripe webhook delivery is delayed, ensuring expired subscriptions are caught in near-real-time. ::: ### Grace Period Behavior Lightning Enable relies on Stripe's built-in retry and grace period logic: - **Stripe retries** failed payments automatically according to your Stripe account's [Smart Retries](https://stripe.com/docs/billing/revenue-recovery/smart-retries) settings (typically 3-4 attempts over several days). - **During retries**, the subscription status transitions to `past_due`. API access is **blocked** during this period. - **If all retries fail**, Stripe marks the subscription as `canceled` or `unpaid` depending on your Stripe settings. - **There is no additional grace period** built into Lightning Enable beyond what Stripe provides. The moment the subscription status leaves `active` or `trialing`, API access is blocked. This applies to card subscriptions. A sats-paid account has no dunning: its period ends on `currentPeriodEnd`, and a daily job moves it to the Free Producer Sandbox (see [L402 sats subscription](#l402-sats-subscription)). To restore access after a lapsed subscription: 1. Update your payment method via the Stripe customer portal. 2. Or subscribe again at [lightningenable.com](https://lightningenable.com). 3. Or pay in sats at `POST /api/merchant/subscription/renew` (see [Renew in sats](/getting-started/activate-with-lightning#renew-in-sats)). This works only once the card subscription is canceled: a `past_due` or `unpaid` subscription can still charge the card, so a sats renewal is refused (`409 stripe_subscription_active`) until you cancel it. ### Exempt Paths These paths are never subject to subscription enforcement, even for expired accounts: | Path | Reason | |------|--------| | `/api/stripe/create-checkout-session` | Must be accessible to (re)subscribe | | `/api/stripe/customer-portal` | Must be accessible to manage billing | | `/api/stripe/subscription` | Must be accessible to check status | | `/api/stripe/pricing` | Public pricing information | | `/api/webhooks/stripe` | Incoming Stripe webhooks | | `/api/webhooks/opennode` | Incoming OpenNode webhooks | | `/api/webhooks/strike` | Incoming Strike webhooks | | `/api/l402/pricing` | L402 demo pricing (uses L402 token auth) | | `/api/l402/status` | L402 demo status (uses L402 token auth) | | `/api/l402/demo` | L402 demo endpoint (uses L402 token auth) | | `/api/l402/premium-data` | L402 demo premium data (uses L402 token auth) | | `/api/l402/content` | L402-protected premium guides (uses L402 token auth) | | `/api/manifests` | Public manifest registry | | `/l402/test` | L402 public test endpoint (1-sat ping) | | `/api/merchant/subscription/renew` | Must be accessible so a lapsed account can pay in sats | | `/health` | Health check | | `/swagger` | API documentation | Only the specific L402 demo paths listed above are exempt. The L402 producer API (`/api/l402/challenges` and its `/verify` routes) **is** subscription-enforced and feature-gated: a Free account reaches it through the Free carve-out (within the Free caps), and a paid account needs a valid subscription. See [L402 Feature Gating](#l402-feature-gating). This ensures merchants can always manage their subscription and billing even when their API access is blocked. --- ## Evaluating without a subscription Two paths give you API access with no Stripe subscription. Both are self-serve; neither needs an administrator. | Path | Tier | Duration | Limits | |------|------|----------|--------| | [Free Producer Sandbox](https://api.lightningenable.com/dashboard/signup) | `free` | Indefinite | 3 endpoints, 200 challenges/month, 1,000 sats max per challenge, 1 proxy | | [L402 Fast Lane](/getting-started/activate-with-lightning) | `individual` | 30 days | None — a full Individual trial | The admin-created `pilot` tier that used to serve this purpose was removed in September 2026. An account still stored as `pilot` resolves to `free` and takes the Free carve-out, so it keeps working at Free capacity. --- ## Feature Gating Beyond subscription status, the middleware enforces plan-specific feature access on certain endpoints. ### Gated Features Each gate reads a **per-account flag**, not the plan table. Plan changes set those flags, but an operator can also set them individually. | Feature | Gated Endpoint | Account flag | `required_plan` | `action_required` | |---------|----------------|--------------|-----------------|-------------------| | `refunds` | `/api/refunds/*` | `refundsEnabled` | `null` | `contact_support` | | `l402` | `/api/l402/challenges*` | `l402Enabled` | `individual` | `upgrade_plan` | If a merchant attempts to access a gated endpoint without the required feature flag, they receive: ```json { "error": "Feature not available", "message": "Refund processing is not enabled for your account. Please contact support.", "feature": "refunds", "current_plan": "free", "required_plan": null, "action_required": "contact_support" } ``` :::note Refunds are never granted by a plan No plan sets `refundsEnabled` — it is an operator-granted per-account flag. The refunds `403` therefore sends `required_plan: null` and `action_required: "contact_support"`, because upgrading would not turn the feature on. Contact support@lightningenable.com instead. ::: :::note Key on `feature`, not on the tier name Key your error handling on the `feature` field. `current_plan` is the **normalized** tier: it is always one of `free`, `individual`, or `l402` for an account whose tier we recognize, even when the stored value is a retired spelling. There are two exceptions: an unrecognized tier value is echoed back raw so an operator can see what needs fixing, and a blank tier (no plan on file) is reported as `null`. ::: ### Feature Flags in Context When a request passes all subscription and feature checks, the middleware populates `MerchantFeatures` in the request context. Controllers can use these flags for fine-grained access control: | Feature Flag | Type | Description | |-------------|------|-------------| | `RefundsEnabled` | boolean | Can process refunds | | `MultiCurrencyEnabled` | boolean | Always `true`. Every plan may invoice in any supported currency; the field is kept for compatibility and gates nothing | | `MaxWebhookEndpoints` | int | Maximum webhook endpoints allowed | | `AnalyticsEnabled` | boolean | Access to analytics | | `PrioritySupport` | boolean | Priority support access | | `CustomBrandingEnabled` | boolean | Custom branding on checkout | ### L402 Feature Gating L402 server-side features (creating proxies, configuring endpoint pricing, the producer API) are available on **every** live tier. Access is controlled by the `l402Enabled` flag on the merchant account, which the plan sets to `true` on all three. What differs is capacity, not availability: Free is capped, both paid plans are not. Check your L402 status: ```bash curl https://api.lightningenable.com/api/merchant/l402-status \ -H "X-API-Key: YOUR_API_KEY" ``` | Plan | L402 Server-Side | Price | |------|------------------|-------| | Free Producer Sandbox | **Yes** — capped at 3 endpoints, 200 challenges/mo, 1,000 sats per challenge | $0 | | Agentic Commerce | **Yes** — uncapped | $49/mo | | Agentic Commerce — Business | **Yes** — uncapped | Contact us | :::tip MCP Tools Are Free The MCP server's L402 *client* tools (`access_l402_resource`, `pay_l402_challenge`) are free for everyone. No subscription is needed to *pay* L402 invoices -- only to *create* L402-protected endpoints. ::: --- ## Handling Subscription Errors in Your Integration ### Detecting Subscription Issues All subscription-related errors return HTTP `403` with an `action_required` field. Use this field to determine the appropriate response: | `action_required` | Meaning | Recommended Action | |-------------------|---------|-------------------| | `contact_support` | Account deactivated, **or** a feature that no plan grants (refunds) | Contact support@lightningenable.com. Do not offer an upgrade | | `subscribe` | No active subscription | Redirect to subscription page | | `update_payment_method` | Payment failed | Redirect to Stripe customer portal | | `renew_subscription` | Subscription expired or canceled | If the body has `renew.endpoint`, pay there in sats; otherwise redirect to subscription page | | `upgrade_plan` | Feature requires a different plan | Show upgrade options for `required_plan` | Branch on `action_required`, not on `error`. `contact_support` covers two different causes, and `error` tells you which: `"Account inactive"` versus `"Feature not available"`. ### Example Error Handler ```javascript async function callLightningEnableApi(endpoint) { const response = await fetch(`https://api.lightningenable.com${endpoint}`, { headers: { 'X-API-Key': process.env.LIGHTNING_API_KEY } }); if (response.status === 403) { const error = await response.json(); switch (error.action_required) { case 'subscribe': case 'renew_subscription': console.error('Subscription issue:', error.message); // Redirect user to subscription page break; case 'update_payment_method': console.error('Payment issue:', error.message); // Redirect user to Stripe customer portal break; case 'upgrade_plan': // required_plan is null when no plan grants the feature, so guard it. if (error.required_plan) { console.error(`Feature "${error.feature}" requires the ${error.required_plan} plan`); // Show upgrade options } else { console.error(`Feature "${error.feature}" is not available on any plan:`, error.message); // Point the user at support, not at checkout } break; case 'contact_support': // Two causes: a deactivated account, or a feature no plan grants // (refunds). error.feature is present only for the second. if (error.feature) { console.error(`Feature "${error.feature}" must be enabled by support:`, error.message); } else { console.error('Account issue:', error.message); } break; } throw new Error(error.message); } return response.json(); } ``` --- ## Next Steps - [Product Overview](/products/product-overview) -- Compare plan features - [Error Code Reference](/api-reference/errors) -- All API error codes - [Merchant Settings](/api-reference/merchant-settings) -- Check subscription status via API - [FAQ](/faq) -- Common questions about plans and pricing ============================================================================== # Release Notes Source: https://docs.lightningenable.com/release-notes ============================================================================== # Release Notes ## API ### Caller-set invoice expiry (October 2026) `POST /api/payments` takes an optional `expiresInSeconds` (60 to 86400) that sets how long the invoice stays payable. Without it, your payment provider's default lifetime applies. - **`expiresAt` now reports the invoice's real deadline**, read from the BOLT11 itself, on every provider. For Strike merchants this is a **behavior change**: the stored and returned `expiresAt` used to be a fixed 60 minutes after creation, but a Strike invoice minted without `expiresInSeconds` stays payable for about 24 hours. `expiresAt`, `GET /api/payments/{invoiceId}` and the `/pay` page countdown (now shown as `h:mm:ss` from one hour up) all show that real deadline. Send `expiresInSeconds` if you want the old one-hour window - **A provider failure is a `502`**: when Strike refuses to create the invoice, `POST /api/payments` returns `502 payment_provider_rejected` with the provider's reason, instead of a bare `500`. A provider outage (a 5xx from Strike) returns `502 payment_provider_unavailable` and asks you to retry later. Both carry `retryable` - An `expiresInSeconds` outside 60 to 86400, or one that isn't an integer, returns `400 invalid_expiry` ### Repricing (September 2026) The publicly marketed paid plan (tier id `individual`) moved from **$99/month to $49/month** and lost the "— Individual" suffix from its display name — it now shows simply as **Agentic Commerce**. Annual billing was dropped: no plan advertises an annual price. - **Agentic Commerce — Business** (tier id `l402`, previously $299/month) went **contact-only**: it is de-listed from public pricing pages and from the pricing feeds — `GET /api/stripe/pricing` and `GET /api/bitcoin/pricing` now return only the publicly listed plans, so Business no longer appears in either response. `POST /api/stripe/create-checkout-session`, `POST /api/bitcoin/create-subscription`, `/Checkout`, and `/BitcoinCheckout` all reject a new Business subscription attempt with a `400` pointing to support@lightningenable.com. Existing Business subscribers, their renewals, and the admin endpoint (`PUT /api/admin/merchants/{id}`) are unaffected - **Free Producer Sandbox** (tier id `free`, $0, no card) is promoted as the default way to start: 3 endpoints, 200 challenges/month, 1,000 sats maximum per challenge - Tier ids are unchanged — existing accounts on any tier keep working exactly as before ### Plan Tier Consolidation (September 2026) Six plan tiers collapsed to three: **`free`** (Free Producer Sandbox), **`individual`** (Agentic Commerce — Individual), and **`l402`** (Agentic Commerce — Business). The `pilot`, `standalone`, and `standard` (Kentico Commerce) tiers were removed after a production snapshot confirmed no account was on any of them. Prices are unchanged. - **Retired tier ids still resolve, but cannot be selected** — an account or an old link carrying `standard`, `kenticocommerce`, `standalone`, or `standaloneapi` resolves to `individual`, and `pilot` resolves to `free`, so nothing that worked stops working. Retired paid ids resolve to Individual rather than Free so an existing subscriber is never silently downgraded - **`planTier` is now the normalized value** on `GET /api/merchant/me`, `GET /api/merchant/l402-status`, and `GET /api/merchant/subscription` — a **contract change**. These three fields previously returned the raw stored column, so the same unset account was reported as `pilot` by two of them and `standalone` by the third. They now always return one of the three live tier ids. Update any client that string-matches a retired id - **`GET /api/stripe/pricing` returns both paid plans** — it previously returned exactly one, the retired Kentico tier that could not be purchased (since the September 2026 repricing it returns only Agentic Commerce) - **Checkout endpoints accept the canonical `l402`** — `POST /api/stripe/create-checkout-session` and `POST /api/bitcoin/create-subscription` take `individual`, `l402`, or `l402microtransactions`, and reject a retired spelling with a `400` instead of failing later in the service (since the September 2026 repricing they accept only `individual`) - **The refunds `403` no longer names a plan** — it sends `required_plan: null` and `action_required: "contact_support"`. No plan grants refunds; `refundsEnabled` is a per-account flag an operator sets on request, so the previous `upgrade_plan` sent customers to buy an upgrade that would not have helped - **`PUT /api/admin/merchants/{id}` rejects an unrecognized or blank `planTier`** with a `400` naming the accepted values, rather than persisting a typo. A retired spelling is still accepted and rewritten to the live tier - **Display fixes** — the `/Success` page after a Stripe checkout shows "Agentic Commerce — Business" instead of the raw `l402microtransactions`; welcome and payment emails no longer quote the retired Kentico plan or fall back to "Free" at $0.00 after a payment; `/Checkout?plan=standalone` preselects Individual instead of Business --- ### Documentation Accuracy Overhaul (July 2026) Full reconciliation audit of the docs site against the shipped product, fixing pages that had drifted from the code. - Corrected the documented **webhook**, **refund**, and **payment** API contracts to match actual response shapes and endpoints - Removed endpoints and features that never shipped (including a phantom Exchange Rates API) and fixed plan-matrix contradictions - Removed internal runbook content and any tutorial patterns that put API keys in browser-side code - Backfilled these release notes — the API section had not been updated since March 2026 --- ### L402 Protocol Fixes (June 2026) - **Caveat intersection** — repeated macaroon caveats now intersect (monotonic narrowing) instead of last-write-wins, closing a scope-widening hole - **Proxy macaroon scoping** — proxy macaroon verification is bound to the merchant and path it was issued for - **Spec-compliant identifier encoding** — the macaroon identifier is encoded as raw bytes per the L402 spec, improving interoperability with other L402 tooling --- ### Security Hardening (May–June 2026) Fixes from the May 2026 security audit, plus CI scanning. - **Per-IP auth-failure throttle** — repeated failed authentication attempts from one IP return 429 with a `Retry-After` header (defaults: 20 failures per 60-second window); also covers the Hangfire dashboard - **Constant-time comparisons** on all secret checks (API keys, admin key, webhook signatures) - **Public checkout status endpoints** trimmed to return only `{ status }` — enumeration-safe, no side effects - **Antiforgery enforcement** on dashboard login and magic-link forms - **HSTS header** (staged, config-driven rollout; June 2026) - CodeQL, Semgrep, and gitleaks security scanning added to CI --- ### Native L402 Server SDKs (May 2026) Native mode launched: monetize your API on your own domain — traffic never flows through Lightning Enable. See the [Native Integration guide](/products/agentic-commerce/native-integration). - **Node.js:** [`l402-server`](https://www.npmjs.com/package/l402-server) (SDK) and [`l402-express`](https://www.npmjs.com/package/l402-express) (Express middleware) - **.NET:** [`L402Server`](https://www.nuget.org/packages/L402Server) (SDK) and [`L402Server.AspNetCore`](https://www.nuget.org/packages/L402Server.AspNetCore) (ASP.NET Core middleware) - Two live, open-source reference apps: [l402-example-node](https://github.com/refined-element/l402-example-node) and [l402-example-aspnet](https://github.com/refined-element/l402-example-aspnet) - New docs: [Express walkthrough](/products/agentic-commerce/native-integration-express), [ASP.NET Core walkthrough](/products/agentic-commerce/native-integration-aspnet), and [Producer API Reference](/products/agentic-commerce/producer-api-reference) --- ### Dashboard: Smart Setup & Test-It Panel (May 2026) - **Smart Setup** — paste an OpenAPI spec URL and the dashboard auto-detects your endpoints to create an L402 proxy - **Test-It panel** — live 402 preview per endpoint, so you can see exactly what agents receive before going live - Merged Pricing tab and self-service **Regenerate API key** button on Settings --- ### Merchant CORS Auto-Allowlist & Canonical Shopify Identity (May 2026) - **Merchant-origin CORS auto-allowlist** — merchant storefront domains are allowed automatically via the `MerchantOrigins` table; no manual CORS configuration per merchant - **Canonical Shopify shop identity** — the `*.myshopify.com` handle and primary domain are read from Shopify's own `shop.json` (never derived heuristically) on OAuth install, credential save, and an explicit dashboard refresh button; admin backfill and scope-probe diagnostics heal legacy rows --- ### Premium Guides & L402 Tool APIs (May 2026) - **Premium guide catalog expanded** and repriced; guides are L402-protected content endpoints — pay per guide with any L402 client (first shipped March 2026) - **Tier 1 L402 tool APIs** added to the registry (retired in September 2026) --- ### Shopify: Draft-Order Tax & Required Buyer Location (April 2026) - **Tax via Shopify Draft Orders** — checkout creates a temporary draft order to get Shopify-calculated tax, which is included in the Lightning invoice total (requires the `write_draft_orders` scope) - **`X-Buyer-Location` header is now required** at Shopify checkout (`{country}-{state}-{zip}`, the buyer's location); the stored default-location fallback was removed - **30-day free trial** on Agentic Commerce plans (card required, no charge until the trial ends) --- ### v1.3.0 — Public L402 Test Endpoint (March 2026) - **Public test endpoint** — `GET https://api.lightningenable.com/l402/test/ping` returns a 402 with a 1-sat invoice, no API key or signup required - Pay the invoice, retry with `Authorization: L402 :`, get a 200 — full L402 round-trip in two curl commands - Alby Hub officially verified as L402-compatible --- ### Shopify L402 Integration (March 2026) AI agents can browse a Shopify catalog, pay via Lightning, and create real Shopify orders — L402 cryptographic proof is the payment verification, no webhook dependency. - Public store endpoints: catalog → checkout (402 + Lightning invoice) → claim with `Authorization: L402 {macaroon}:{preimage}` + shipping address - **Shopify OAuth app install** from the dashboard, with manual token entry preserved as an advanced fallback - Claim tokens allow a configurable window (default 30 days) to provide shipping details after payment; expired unclaimed orders are auto-purged - Active Shopify stores are auto-registered in the L402 API registry for agent discovery - See the [Shopify Commerce docs](/products/shopify-commerce/overview) --- ### v1.2.0 — L402 Producer API (March 2026) Agents can now **earn**, not just spend. The L402 Producer API enables Agentic Commerce subscribers to create L402 payment challenges and verify payments — powering agent-to-agent commerce where AI agents autonomously buy and sell services. #### New Features - **L402 Producer API** — Two new endpoints (`POST /api/l402/challenges`, `POST /api/l402/challenges/verify`) let merchants programmatically create L402 payment challenges and verify L402 tokens - **Challenge idempotency** — Same resource + price from the same client within 60 seconds returns the same invoice, preventing duplicate charges on retries - **Two new MCP tools** — `create_l402_challenge` and `verify_l402_payment` bring producer capabilities directly to AI agents via MCP #### Documentation - New [L402 Producer API guide](/products/agentic-commerce/l402-producer-api) with end-to-end agent-to-agent commerce examples - Updated [MCP Complete Guide](/products/agentic-commerce/mcp-complete-guide) with producer tool reference - Updated [L402 API Reference](/api-reference/l402) with producer endpoints --- ### Strike Payment Provider (February 2026) Multi-provider settlement abstraction: merchants choose Strike or OpenNode as their payment provider. Lightning Enable does not hold funds — the configured provider facilitates custody and settlement. - **Strike is the default provider** on the hosted service (`PaymentProvider:Default`); each merchant can override per-account - Strike returns the **preimage** on outgoing payments, which L402 requires — making it the recommended provider for L402 workloads - Strike webhooks supported (thin payloads — the API fetches full entity details after receipt) - Multi-currency support via Strike (USD, EUR, GBP, AUD, BTC, USDT) --- ### v1.1.0 — Security Audit & Quality Hardening (February 2026) Comprehensive security audit and code quality pass across the entire API surface. Added 244 new tests, bringing total coverage from 407 to 651 tests. #### Security Hardening (7 Critical + 13 High Priority) **Critical fixes:** - **SSRF protection** on webhook callback URLs and proxy target URLs — block internal/private network ranges - **Webhook signature verification** — constant-time HMAC comparison to prevent timing attacks - **L402 amount binding** — macaroon caveats now bind to the invoiced amount, preventing underpayment exploits - **Subscription enforcement** — active Stripe subscription required for all authenticated API operations - **Error sanitization** — internal exception details, stack traces, and infrastructure information no longer leak in API responses - **API key hashing** — merchant API keys stored as one-way hashes for improved credential security - **Webhook replay protection** — timestamp validation rejects stale webhook deliveries **High priority fixes:** - Rate limiting on authentication, invoice creation, and webhook endpoints - Idempotency keys on all OpenNode charge creation calls to prevent duplicate invoices - Request size limits on all endpoints accepting request bodies - Input validation on all merchant-supplied URLs (scheme, length, format) - Correlation ID propagation via `X-Correlation-Id` header for end-to-end request tracing - Webhook delivery queue hardening with dead-letter handling and bounded retries - L402 macaroon expiry enforcement and nonce replay protection - Proxy target URL allowlist validation against merchant-registered domains - Merchant-scoped database query enforcement across all repository methods - Stripe webhook signature verification on all subscription lifecycle events - OpenNode API key validation on merchant registration - Secure cache headers on all API responses - Admin endpoint authentication audit and header validation #### API Improvements (10 Medium Priority) - OpenAPI annotations on all endpoints with response type documentation - Cancellation token propagation through all async controller actions and service methods - DTO validation attributes on all request models (required fields, range constraints, URL format) - Structured logging with Serilog semantic properties across all services - Consistent error response format using `ProblemDetails` (RFC 9457) - Health check endpoint expanded with dependency status (database, OpenNode, Stripe) - Webhook delivery status tracking with queryable history per merchant - Rate-limited (429) responses include a `retryAfter` hint (seconds) in the JSON error body - Request/response logging middleware with PII redaction - Graceful shutdown handling for in-flight webhook deliveries and background jobs #### Code Quality (5 Low Priority) - Dead code removal across controllers, services, and middleware - Consistent `async`/`await` patterns — eliminated fire-and-forget calls - Nullable reference type annotations on all public API surfaces - Standardized exception hierarchy with domain-specific exception types - Code style enforcement via `.editorconfig` and analyzer rules #### Test Coverage - **244 new tests** added (407 to 651 total) - Security-focused tests: SSRF blocking, signature verification, replay protection, rate limiting - Integration tests for full webhook delivery pipeline - L402 protocol tests covering challenge, payment, and verification flows - Subscription enforcement tests across all plan tiers - Edge case coverage for concurrent requests, timeout handling, and malformed input --- ## MCP Server ### Docs Tool and Aligned Budget Defaults (MCP 2.2.0, 2026-11-04) - **New tool: `docs`** searches and reads this documentation site from inside the server (`action="search"|"read"`), so an agent without web access can look things up. No wallet or API key needed. See [docs](/products/agentic-commerce/mcp-complete-guide#docs). - **One set of budget defaults in both ports.** The .NET and Python servers now write the same conservative `config.json` on first run and fall back to it for missing keys: auto-approve $0.10, log-and-approve $1, first-payment approval on. Out-of-order tiers are clamped down, never raised. See [Budget Configuration](/products/agentic-commerce/mcp-complete-guide#budget-configuration). - **Hosted startup warning.** With `LIGHTNING_ENABLE_HOSTED=1` and no confirmation channel, the server warns at startup that no payment can succeed, and names the fixes. ### Build Pipeline Hardening (MCP 2.1.1, 2026-10-22) No change to how either server behaves. The release pipeline and the Docker image now verify and pin their installers, and the image is rebuilt from a digest-pinned base. ### Renew in Sats (MCP 2.1.0, 2026-10-11) - **New tool: `renew_lightning_enable_subscription`** pays the L402 challenge from `POST /api/merchant/subscription/renew` to buy or extend 30 days of Agentic Commerce in sats. It needs `LIGHTNING_ENABLE_API_KEY` and a wallet that returns a preimage. See [Renew in sats](/getting-started/activate-with-lightning#renew-in-sats). ### Tool Consolidation — Lite/Standard/Full Profiles (MCP 2.0.0, September 2026) The one-tool-per-operation surface is replaced by three selectable profiles, set with `LIGHTNING_ENABLE_TOOL_PROFILE`: **`lite`** (`pay_invoice`, `access_l402_resource`, `get_balance`, `budget`, `receipts`, `setup_wallet`), **`standard`** (the new default — adds `pay_l402_challenge`, `test_l402_payment`, `create_invoice`, `check_invoice_status`, `verify_confirmation_code`, `discover_api`, `create_lightning_enable_account`, `wallet_ops`, `l402_producer`, `agent_services`), and **`full`** (`standard` plus every pre-consolidation tool name as a deprecated alias, for prompts and integrations written before this change). - **New consolidated, action-based tools:** `budget` (`action="status"|"tighten"`, replaces `get_budget_status` + `configure_budget`), `receipts` (`source="durable"|"session"`, replaces `get_receipts` + `get_payment_history`), `wallet_ops` (`action="price"|"exchange"|"send_onchain"`, replaces `get_btc_price` + `exchange_currency` + `send_onchain`), `l402_producer` (`action="create"|"verify"|"configure_receive"|"status"|"create_proxy"|"add_endpoint"|"publish"|"list_challenges"`, replaces `create_l402_challenge` + `verify_l402_payment` and adds six new producer-setup actions), and `agent_services` (`action="discover"|"request"|"settle"|"publish"|"unpublish"|"attest"|"reputation"`, replaces all seven ASA tools). - **New tool: `setup_wallet`** — configures `~/.lightning-enable/config.json` (wallet credential, spend ceiling) without hand-editing JSON. - **Every pre-consolidation tool name** (the names replaced above, plus the `check_wallet_balance`/`get_all_balances`/`confirm_payment` aliases from v1.17.0) stays callable as a deprecated alias under every profile, scheduled for removal in **v3.0.0**. Profiles change only what the server lists: `LIGHTNING_ENABLE_TOOL_PROFILE=full` lists the consolidation aliases, and `standard` and `lite` don't. - **Out-of-band confirmation gains a `confirmation.channel` config option** (`stderr` default, `refuse`, `webhook`, `file`), and `LIGHTNING_ENABLE_HOSTED=1` defaults a non-TTY process to `refuse` instead of `stderr`. - **Sats-native budget keys** — `limits.maxPerPaymentSats` / `limits.maxPerSessionSats` and `tiers.autoApproveSats` — sit alongside the existing USD-denominated ones for operators who'd rather not depend on the live BTC price feed for their ceiling. - The durable receipt log is now also exposed as an MCP **Resource** at `lightning-enable://receipts`. - See the [MCP Complete Guide](/products/agentic-commerce/mcp-complete-guide) for the full current tool list and the [deprecated alias table](/products/agentic-commerce/mcp-complete-guide#deprecated-aliases). ### v1.24.0 – v1.24.1 — Modern MPP "Payment" Scheme + Discovery Fixes (August 2026) - **MPP draft-00 client support, both ports:** modern `Payment` challenges (`draft-httpauth-payment-00`) are parsed and answered with the single-use `Authorization: Payment ` credential, with client-side safety checks (expiry, `intent: charge`, sat currency, amount-vs-invoice agreement). `access_l402_resource` surfaces the server's `Payment-Receipt`; `pay_l402_challenge` accepts a raw challenge via `challengeHeader` / `challenge_header`. - **`discover_api` probe alignment (v1.24.1):** manifests are also probed at `/.well-known/l402.json`, and protocol signposts are no longer mistaken for manifests. - NWC multi-relay failover on connect (.NET). ### v1.17.0 – v1.23.x — Tool Consolidation, Marketplace, Receipts (July–August 2026) - **Renamed/merged tools (v1.17.0):** `confirm_payment` → `verify_confirmation_code` (it only ever *verified* a code — it never moved money), and `check_wallet_balance` + `get_all_balances` → `get_balance` (a single tool returning the superset of both). Old names keep working as hidden deprecated aliases until **v2.0.0**; responses carry a `deprecated` marker. - **`unpublish_agent_capability`** joined the ASA surface (take a listing down: retires the L402 proxy, publishes a NIP-09 deletion + status=removed replacement). See the [MCP Complete Guide](/products/agentic-commerce/mcp-complete-guide) for the current tool list. - **`get_receipts`** — durable local receipt log for payments made through the server. - Removed the legacy `L402_MAX_SATS_PER_REQUEST` / `L402_MAX_SATS_PER_SESSION` environment variables; operator limits live in `~/.lightning-enable/config.json` (USD-denominated), with `configure_budget` tighten-only at runtime. - NWC hardening: multi-relay connection strings (Alby Hub), SSRF connect guards, encryption auto-detect refinements. ### v1.15.0 — Self-Serve Signup (July 2026) - **`create_lightning_enable_account`** — self-bootstrapping signup: pays the ~100-sat L402 Fast Lane challenge (`POST /api/signup/l402`) with the connected wallet, returns a merchant API key with a 30-day trial, and writes it to `~/.lightning-enable/config.json` so the producer/ASA tools unlock on next restart. No card, no browser. ### v1.12.13 — Destination-Bound Confirmation Codes (June 2026) Security release — update both packages. - **Confirmation codes are now bound to the payment destination**, in addition to the amount and tool: the BOLT11 invoice (`pay_invoice`, `pay_l402_challenge`), the resource URL (`access_l402_resource`), the on-chain address (`send_onchain`), and the settlement endpoint (`settle_agent_service`, Python). - Closes an anti-redirect hole: previously a prompt-injected agent could obtain a code for a benign payment, then re-call the same tool at the same amount with a **different destination**. Destination mismatch now rejects without consuming the code, so the legitimate retry still works. - Applies to **both** the .NET and Python packages. --- ### v1.12.12 — Python Funds-Safety Parity (June 2026) Brings the **Python** package in line with the .NET out-of-band confirmation that shipped in v1.12.10. - **Out-of-band confirmation now in Python too.** Above-threshold payments (`pay_invoice`, `access_l402_resource`, `pay_l402_challenge`) print the confirmation code to the server console/stderr only — never in a tool result — so a prompt-injected agent can't read its own code and self-approve. Before this release the Python package still accepted an agent-supplied confirmation flag. - **`send_onchain` always confirms and fails closed** in Python (parity with .NET): no budget service, or any budget-check error, refuses the send. - **Confirmation parameter unified** across both packages — `.NET` uses `confirmationNonce`, Python uses `confirmation_nonce` (same concept, each language's casing). - The legacy in-process budget manager now fails closed above the auto-approve floor instead of allowing a self-confirm. --- ### v1.12.11 — Python Packaging Fix (June 2026) - `secp256k1` is now an **optional** dependency. `pip install lightning-enable-mcp` works on **every platform, including Windows**, with no build toolchain. - **Nostr Wallet Connect (NWC)** wallets need the optional extra: `pip install lightning-enable-mcp[nwc]`. Using an NWC wallet without it raises a clear error telling you to install it. Other wallet types (LND, Strike, OpenNode) don't need the extra. - The .NET package is unaffected — it uses managed crypto. --- ### v1.12.10 — Funds-Safety Hardening (June 2026) Hardens the agent payment path so a prompt-injected agent can't approve its own large payments. - **Out-of-band confirmation** *(.NET in this release; Python parity in v1.12.12)*. When a payment exceeds the auto-approve threshold, the server prints a confirmation code to its **console / stderr** — visible to the human operator and **never returned in a tool result**. The agent must ask the human for the code, then re-call the original payment tool with its confirmation-nonce parameter (`confirmationNonce` in .NET, `confirmation_nonce` in Python) to proceed. (The separate `confirm_payment` tool only *verifies* a code — it does not execute the payment.) Applies to `pay_invoice`, `access_l402_resource`, and `pay_l402_challenge`. This assumes the AI runtime can't read the server's stderr/logs; for agents that share a shell/host with the server, run it where the agent can't read its stderr. - **`send_onchain` always requires confirmation** (on-chain payments are irreversible), even for small amounts, and **fails closed** if the budget service is unavailable. - **Confirmation codes are bound to the exact amount AND tool** they approved — no cross-tool or cross-amount reuse. - **`configure_budget` added to the .NET server** (previously Python-only) and is **tighten-only in both packages**: an agent can lower its per-request / per-session caps at runtime but can never raise them above the operator's `~/.lightning-enable/config.json` limits. - **Budget checks fail closed** if the BTC price feed is unavailable (3 price sources, no stale fallback). - The NWC response preimage is no longer logged. --- ### v1.12.2 — Critical: Payment Confirmation Fix (April 2026) **Severity:** Critical — affects all .NET MCP clients since v1.6.0 **Problem:** When a client reports MCP elicitation capability (as Claude Code does) but elicitation doesn't actually work, the payment confirmation flow returned "Payment cancelled by user" with no nonce and no recovery path. Any payment above the auto-approve threshold was permanently blocked. **Fix:** Always fall back to nonce-based confirmation when elicitation fails, regardless of reported client capabilities. Affected tools: `pay_invoice`, `access_l402_resource`, `pay_l402_challenge`. **Update immediately:** ```bash # .NET global tool dotnet tool update -g LightningEnable.Mcp ``` **Affected versions:** v1.6.0 through v1.12.1 (all deprecated on NuGet). Python package was never affected. --- ### v1.11.2 — Version Bump (March 2026) - Version bump for CI pipeline alignment; no functional changes --- ### v1.11.1 — L402 HTTP Client Fix (March 2026) - Fixed gzip decompression issue in the L402 HTTP client by sending `Accept-Encoding: identity`, preventing decompression errors on some servers --- ### v1.11.0 — NIP-44 v2 Outgoing Encryption (March 2026) - NWC outgoing requests now encrypted with NIP-44 v2 (Alby Hub compatibility) - NIP-47 encryption tag support for improved NWC interoperability --- ### v1.10.1 — Docker Image Update (March 2026) - Updated Docker base image; no functional changes --- ### v1.10.0 — NIP-44 v2 Incoming Decryption (March 2026) - Auto-detects NIP-04 vs NIP-44 v2 encryption on incoming NWC responses - No configuration required — works transparently with all supported NWC wallets --- ### v1.9.0 — Producer Tools (March 2026) - **`create_l402_challenge`** — AI agents can now sell services: create a Lightning invoice + macaroon to present to other agents or users as a 402 challenge - **`verify_l402_payment`** — Verify an L402 token (macaroon + preimage) to confirm payment before granting access - Both tools require an Agentic Commerce subscription (from $99/mo) --- ### v1.8.0 — LND Wallet Support (February 2026) - Added LND REST API wallet backend for self-hosted nodes - Configure with `LND_REST_HOST` and `LND_MACAROON_HEX` env vars - Full L402 preimage support via LND --- ### v1.7.0 — `discover_api` Tool (February 2026) - **`discover_api`** — Search the L402 API registry by keyword/category, or fetch a specific API's manifest from a URL - Budget-aware annotations show how many calls you can afford at the current BTC price --- ### v1.6.0 — License Removed, All Consumer Tools Free (February 2026) - **License requirement removed** — all 15 consumer tools are now free, no Lightning payment or subscription required - Added **`confirm_payment`** tool for explicit payment confirmation before execution - The 6,000-sat license purchase from v1.5.0 is no longer needed; existing licenses are ignored --- ### v1.5.2 — Version Display Fix (February 2026) - Assembly version now matches package version for accurate startup display - Includes all v1.5.1 fixes below --- ### v1.5.1 — Critical Bug Fix (February 2026) **Severity:** Critical — affects all payment confirmations **Problem:** The `confirm_payment` tool was consuming the payment nonce before `pay_invoice` could use it. This caused every confirmed payment to fail with: ``` Invalid, expired, or already-used confirmation nonce ``` **Root Cause:** Both `confirm_payment` and `pay_invoice` called `ValidateAndConsumeConfirmation()`, which removes the nonce from memory. Since `confirm_payment` runs first (to get user approval), it consumed the nonce, leaving nothing for `pay_invoice` to validate against. **Fix:** `confirm_payment` now uses a read-only `ValidateConfirmation()` method that checks the nonce without consuming it. Only `pay_invoice` consumes the nonce. **Update immediately:** ```bash # .NET global tool dotnet tool update -g LightningEnable.Mcp ``` **Affected versions:** v1.5.0 --- ### v1.5.0 — Multi-Wallet Support - Added Strike wallet backend with preimage support - Added LND REST wallet backend - Added Nostr Wallet Connect (NWC) wallet backend - Configurable wallet priority via `WALLET_PRIORITY` env var or config file - Config file support at `~/.lightning-enable/config.json` - L402 license purchase via Lightning payment (6,000 sats, valid forever) - Budget controls with dual USD/sats limits --- ### v1.4.0 — L402 Auto-Pay - `access_l402_resource` tool for automatic L402 payment - `pay_l402_challenge` tool for manual L402 payment - Budget enforcement with per-request and per-session limits - Payment history tracking --- ### v1.3.0 — Initial Release - `pay_invoice` — Pay any Lightning invoice - `check_wallet_balance` — Check wallet balance - `get_payment_history` — View payment history - `get_budget_status` — View budget limits - OpenNode wallet backend ============================================================================== # Strike Account Setup Source: https://docs.lightningenable.com/strike-setup/account-setup ============================================================================== # Strike Account Setup Strike is a payments platform that supports Bitcoin Lightning and on-chain payments. Lightning Enable uses your Strike API key to create invoices and track their payment status. :::tip Default provider Strike is the default payment provider: merchants who make no explicit choice are served by Strike. It returns payment preimages (needed for L402) and creates invoices in USD, EUR, GBP, or BTC. ::: Lightning Enable does not hold funds; Strike facilitates custody and settlement. You bring your own Strike API key. ## Create Strike Account ### Step 1: Sign Up 1. Visit [dashboard.strike.me](https://dashboard.strike.me) 2. Create an account or sign in 3. Complete identity verification if prompted ### Step 2: Generate an API Key 1. Go to **API Keys** in the Strike dashboard 2. Click **Create API Key** 3. Name your key (e.g., "Lightning Enable Production") 4. Enable the following scopes: - `partner.receive-request.read` — Read receive requests (invoices) - `partner.receive-request.create` — Create receive requests - `partner.webhooks.manage` — Manage webhook subscriptions 5. Click **Create** and copy your API key :::tip Not sure? We'll set it up with you Scopes are the step people most often get wrong, and a key missing one fails quietly at your first real payment. If anything here is unclear, email [support@lightningenable.com](mailto:support@lightningenable.com) before you go live — a real person reads it, and we're happy to walk through the setup with you. ::: :::warning Save Your API Key Strike only shows the full API key once at creation. Copy it immediately and store it securely. If lost, you'll need to create a new key. ::: ### Step 3: Configure in Lightning Enable 1. Log in to the [Lightning Enable dashboard](https://api.lightningenable.com/dashboard) 2. Go to **Settings** 3. Under **Payment Provider**, select **Strike** 4. Paste your Strike API key 5. Click **Save Key** 6. Click **Validate** to verify the key works ## API Key Scopes All three scopes above are required. **Validate** (Dashboard, or `POST /api/merchant/validate-strike`) reads a receive request and then creates a 1-sat Lightning invoice that expires after 60 seconds, so a key that passes can create invoices. A key that can read but not create fails validation with the missing scope named. `partner.webhooks.manage` is not checked by Validate. Lightning Enable uses it to register a Strike webhook subscription the first time it creates an invoice for you. See [API Keys](/strike-setup/api-keys#required-scopes) for scope details and key rotation. ## Environments Lightning Enable's hosted platform connects to Strike's production environment: | | Details | |---|---------| | **API URL** | `https://api.strike.me/v1` | | **Dashboard** | `https://dashboard.strike.me` | | **Bitcoin** | Real mainnet Bitcoin | :::info Testing on the Hosted Platform Strike does offer a sandbox environment (`https://api.dev.strike.me/v1`), but the Strike API base URL on Lightning Enable's hosted platform is a platform-wide setting pointed at production — it is not selectable per merchant. The practical way to test your integration is to create a small real invoice (e.g., $1 USD) and pay it with a Lightning wallet. ::: ## How Payments Work with Strike When a customer pays through Lightning Enable with Strike as the provider: 1. Lightning Enable calls Strike's **receive-requests** API to create an invoice 2. Strike returns both a **Lightning invoice** (BOLT11) and an **on-chain Bitcoin address** 3. The customer chooses to pay via Lightning or on-chain 4. Strike sends a **webhook** when payment is received 5. Lightning Enable updates the payment status and notifies your platform ### Payment Methods | Method | Speed | Best For | |--------|-------|----------| | **Lightning** | Instant (~1 second) | Small-to-medium payments | | **On-chain** | ~10+ minutes (1 confirmation) | Larger payments, fallback | Both methods are presented on the payment page; the customer chooses. ## Refunds with Strike Strike has no refund API, so Lightning Enable sends a refund as a new outgoing payment (Lightning or on-chain) from your Strike balance. Refunds are enabled per account; email [support@lightningenable.com](mailto:support@lightningenable.com) to turn them on. ## Account Security - Never share your API key publicly. Keep any copy in a password manager or secret store; Lightning Enable encrypts the saved key at rest (AES-256-GCM). - Rotate keys periodically and revoke unused keys in the Strike dashboard. ## Troubleshooting ### API Key Validation Failed 1. Check the key was copied without extra spaces. 2. Check the key hasn't been revoked in the Strike dashboard. 3. If the message names a missing scope, create a new key with all three scopes. ### Webhooks Not Arriving 1. Confirm the key has `partner.webhooks.manage`. 2. Check your Strike dashboard for a webhook subscription. 3. Re-save your Strike key, then create a new payment. Saving a key clears the stored subscription so the next invoice registers a fresh one. ## Next Steps Once your Strike account is configured: - [Strike Webhooks](/strike-setup/webhooks) — How payment notifications reach your callback URL - [First Payment](/getting-started/first-payment) — Test your integration end to end with a small payment - [Agentic Commerce](/products/agentic-commerce/overview) — Set up pay-per-request APIs - [Dashboard Guide](/products/agentic-commerce/dashboard-guide) — Configure your first proxy ============================================================================== # API Keys Source: https://docs.lightningenable.com/strike-setup/api-keys ============================================================================== # Strike API Keys Your Strike API key lets Lightning Enable create invoices and read payment status on your behalf. Lightning Enable stores the key encrypted and never returns it back out of the API. Strike is the default payment provider. If you have not created your Strike account yet, start with [Strike Account Setup](/strike-setup/account-setup). ## Required Scopes Lightning Enable needs three scopes. Grant all three. **Validate** (`POST /api/merchant/validate-strike`) proves the two receive-request scopes: it reads a receive request, then creates a 1-sat invoice that expires after 60 seconds. It does not exercise `partner.webhooks.manage`, so grant that one at creation. | Scope | Purpose | Required? | |-------|---------|-----------| | `partner.receive-request.read` | Check invoice and payment status | Yes | | `partner.receive-request.create` | Create Lightning invoices | Yes | | `partner.webhooks.manage` | Auto-register the webhook subscription | Yes | :::tip Stuck on scopes? Ask us This is the step people most often get wrong. Email [support@lightningenable.com](mailto:support@lightningenable.com) with what you are seeing in the Strike dashboard and we will tell you exactly which boxes to tick — or set it up with you on a call if that is easier. ::: ## Generate a Key 1. Sign in to [dashboard.strike.me](https://dashboard.strike.me) 2. Go to **API Keys** 3. Click **Create API Key** 4. Name the key so you can identify it later, for example `Lightning Enable Production` 5. Enable all three scopes from the table above 6. Click **Create** and copy the key :::warning Strike Shows the Key Once Strike displays the full API key only at creation. Copy it immediately and store it in a password manager or secret store. If you lose it, create a new key — you cannot recover the original. ::: ## Configure in Lightning Enable There are two ways to save your key, and no others. There is no environment variable or configuration file to set. ### Using the Dashboard 1. Sign in to the [Lightning Enable dashboard](https://api.lightningenable.com/dashboard) 2. Go to **Settings** 3. Under **Payment Provider**, select **Strike** 4. Paste your key and click **Save Key** 5. Click **Validate** to confirm the key works ### Using the Merchant API Save the key: ```bash curl -X PUT https://api.lightningenable.com/api/merchant/strike-key \ -H "X-API-Key: your-merchant-api-key" \ -H "Content-Type: application/json" \ -d '{ "strikeApiKey": "your-strike-api-key" }' ``` Validate it: ```bash curl -X POST https://api.lightningenable.com/api/merchant/validate-strike \ -H "X-API-Key: your-merchant-api-key" ``` Saving a Strike key through either method above already sets Strike as your provider if you had not chosen one. To set it explicitly at any time: ```bash curl -X PUT https://api.lightningenable.com/api/merchant/payment-provider \ -H "X-API-Key: your-merchant-api-key" \ -H "Content-Type: application/json" \ -d '{ "provider": "strike" }' ``` The `provider` field accepts `strike`, `opennode`, or `nwc`. :::info Provider Resolution Lightning Enable resolves your provider in this order: 1. An explicit choice on your account wins. 2. With no explicit choice and exactly **one** credential saved (Strike key, OpenNode key, or NWC connection string), that credential's provider is used. 3. Otherwise, Lightning Enable uses the platform default, which is Strike. Saving a key records that provider as your choice if you had none, so case 3 is rare. To change lanes later, set the provider explicitly. ::: ## Key Security - Configure the key only through the dashboard or `PUT /api/merchant/strike-key`. Never commit it to source control or paste it into client-side code. - Lightning Enable encrypts your key at rest with AES-256-GCM and never returns it from the API once saved. - Keep any copy of the key in a password manager or secret store. - If you suspect the key leaked, revoke it in the Strike dashboard immediately, then save a new one. - Enable two-factor authentication on your Strike account. ## Key Rotation Rotate your key every 90 days, after someone with access leaves, or any time you suspect exposure. 1. Create a new key in the Strike dashboard with all three scopes 2. Save it in Lightning Enable through the dashboard or `PUT /api/merchant/strike-key` 3. Validate it with `POST /api/merchant/validate-strike` 4. Confirm a small real payment succeeds — see [Testing](/strike-setup/testing) 5. Revoke the old key in the Strike dashboard The update takes effect immediately; subsequent payments use the new key. Saving a new key also clears the stored Strike webhook subscription, and the next invoice registers a fresh one against the new key. ## Checking Key Status To confirm a key is saved without revealing it: ```bash curl -X GET https://api.lightningenable.com/api/merchant/me \ -H "X-API-Key: your-merchant-api-key" ``` The `onboarding` object reports `hasStrikeKey` (a boolean, never the key) and `isFullyConfigured`, which is `true` only when your resolved provider has a usable credential. ## Troubleshooting ### Validation Reports `isValid: false` `POST /api/merchant/validate-strike` returns HTTP `200` whether or not the key works — read the `isValid` field in the body rather than the status code. If it reports `false`: 1. Check the key was copied without extra spaces 2. Check the key has not been revoked in the Strike dashboard 3. Confirm a key is actually saved — the same response reports when none is configured 4. Create a new key and save it again A key that can read but not create fails with the missing scope named. See [Required Scopes](#required-scopes). ### Invoices Are Not Created 1. Confirm the key has `partner.receive-request.create` 2. Check that your Lightning Enable subscription is active 3. Look for error detail in the Lightning Enable dashboard ### Webhooks Do Not Arrive See [Strike Webhooks](/strike-setup/webhooks#no-webhooks-arrive-from-strike). ## Next Steps - [Webhooks](/strike-setup/webhooks) — Understand both webhook hops and verify signatures - [Testing](/strike-setup/testing) — Confirm your integration end to end - [First Payment](/getting-started/first-payment) — Take your first payment ============================================================================== # Testing Source: https://docs.lightningenable.com/strike-setup/testing ============================================================================== # Testing Your Strike Integration Work through these checks in order. Each one isolates a different failure point, so the first one that fails tells you where the problem is. ## Before You Start :::info The Hosted Platform Uses Strike Production Strike offers a sandbox at `https://api.dev.strike.me/v1`, but the Strike API base URL on Lightning Enable's hosted platform is a platform-wide setting pointed at production. It is not selectable per merchant. That means testing uses **real mainnet Bitcoin**. Use small amounts — $1 USD or a few hundred sats is enough to prove every step. ::: You need: - A Strike API key saved in Lightning Enable, with all three required scopes - Your Lightning Enable merchant API key - A Lightning wallet holding a small balance ## Step 1: Validate the Key ```bash curl -X POST https://api.lightningenable.com/api/merchant/validate-strike \ -H "X-API-Key: your-merchant-api-key" ``` This returns HTTP `200` whether or not the key works. **Read the `isValid` field in the body** — do not gate on the status code, and do not use `curl -f` here. `"isValid": true` means the key can read receive requests and create invoices (Validate creates a 1-sat invoice that expires after 60 seconds). It does not check `partner.webhooks.manage`; a gap there shows up at Step 4. If `isValid` is `false`, the `message` names the problem. See [API Keys](/strike-setup/api-keys#troubleshooting). ## Step 2: Create a Small Invoice ```bash curl -X POST https://api.lightningenable.com/api/payments \ -H "X-API-Key: your-merchant-api-key" \ -H "Content-Type: application/json" \ -d '{ "orderId": "strike-test-001", "amount": 1.00, "currency": "USD", "description": "Strike integration test" }' ``` `orderId` is required and must be 1–100 characters. It is your own reference for the payment. The response (`201`) includes `invoiceId`, the BOLT11 `lightningInvoice`, `expiresAt`, and `payUrl`, a hosted checkout link you can open to pay. Add `expiresInSeconds` (60 to 86400) to shorten the invoice lifetime; without it, Strike's default is about 24 hours. If this fails, read the error before changing anything: - A `400` naming a field means the request body is wrong. Lightning Enable never contacted Strike. - A `502` with `payment_provider_rejected` means Strike refused the call; the `message` carries Strike's reason. `payment_provider_unavailable` means Strike is down; retry later. :::note Description Length Strike accepts a `description` between 1 and 250 bytes on the invoice. Lightning Enable truncates longer descriptions rather than rejecting them, and substitutes a default when the description is blank. ::: ## Step 3: Pay the Invoice Pay the BOLT11 invoice with your Lightning wallet. Payment settles in about a second. ## Step 4: Confirm the Status Changed ```bash curl -X GET https://api.lightningenable.com/api/payments/{invoiceId}/status ``` The status should read as paid. If it still reads unpaid a minute after your wallet confirms, the Strike webhook is not reaching Lightning Enable — see [Webhooks](/strike-setup/webhooks#no-webhooks-arrive-from-strike). ## Step 5: Confirm Your Callback Fired If you configured a callback URL, check that your endpoint received a signed webhook and that your signature check passed. A delivery that arrives but fails verification is almost always a raw-body problem — see [Verify the Signature](/strike-setup/webhooks#verify-the-signature). ## Testing L402 If you are monetizing an API with L402, Strike is the provider you want. Strike returns the payment preimage on outgoing payments, which matters when an agent pays *from* your balance. Receiving L402 payments works on either provider — challenge creation and verification are provider-agnostic, and verification compares `SHA256(payer's preimage)` against the payment hash without asking your provider for anything. To see the protocol shape before wiring your own endpoint, request the public demo: ```bash curl -i https://api.lightningenable.com/l402/test/ping ``` You get a `402 Payment Required` with a challenge containing a macaroon and an invoice. Pay it, then retry with the token: ```bash curl -i https://api.lightningenable.com/l402/test/ping \ -H "Authorization: L402 {macaroon}:{preimage}" ``` :::warning This Endpoint Does Not Test Your Account `/l402/test/ping` mints and verifies against Lightning Enable's own test merchant. It does not read your API key, your Strike key, or your configuration — the route skips authentication entirely. A `200` here proves the protocol works, not that **your** setup works. It succeeds even if you have no Strike key saved, and it can return `503` for reasons that have nothing to do with your account. To test your own L402 configuration, create a proxy or a producer challenge on your own merchant and pay that. See [Agentic Commerce](/products/agentic-commerce/overview). ::: :::tip One Payment Cycle at a Time Finish one 402 to payment to access cycle before starting the next. Running L402 payments concurrently during testing makes failures much harder to attribute. ::: ## What to Check Before Going Live - [ ] Key validates, and all three scopes are granted - [ ] An invoice is created and paid successfully - [ ] Payment status updates without manual intervention - [ ] Your callback receives webhooks and signature verification passes - [ ] Your webhook handler is idempotent against duplicate deliveries - [ ] Your handler returns `2xx` promptly and does slow work afterward - [ ] For L402: a paid token grants access and an unpaid request is refused ## Troubleshooting ### Invoice Is Created but Never Settles Confirm you paid the BOLT11 invoice rather than an expired copy. Lightning invoices expire; create a fresh one and retry. ### Status Stays Unpaid After a Confirmed Payment The Strike webhook is not arriving. Confirm the `partner.webhooks.manage` scope, then create another payment to trigger re-registration. ### L402 Returns 503 A `503` with `challenge_persist_failed` means Lightning Enable could not durably record the challenge and deliberately issued no invoice. You were not charged. Retry, and if it persists, the platform is reporting a storage fault rather than a problem with your configuration. ### L402 Returns 414 Your resource path exceeds the 848-character limit for a resource identifier. Shorten the path. ## If Something Does Not Add Up You do not have to work through this alone. Email [support@lightningenable.com](mailto:support@lightningenable.com) with the step you are on and what you are seeing — including the correlation ID if you have one — and we will help you finish the integration. We would rather hear from you before your first real payment than after. ## Next Steps - [First Payment](/getting-started/first-payment) — Take a real payment end to end - [Agentic Commerce](/products/agentic-commerce/overview) — Monetize an API per request - [Troubleshooting](/troubleshooting) — Platform-wide diagnostics ============================================================================== # Webhooks Source: https://docs.lightningenable.com/strike-setup/webhooks ============================================================================== # Strike Webhooks Payment notifications travel in two hops. Lightning Enable handles the first; you configure the second. ``` Strike ──(1)──▶ Lightning Enable ──(2)──▶ Your callback URL ``` | Hop | Who configures it | What it carries | |-----|-------------------|-----------------| | **1. Strike → Lightning Enable** | Lightning Enable, automatically | A thin notification with an entity ID | | **2. Lightning Enable → you** | You, with the merchant API | Full payment detail, HMAC signed | ## Hop 1: Strike to Lightning Enable You do not configure this hop. Lightning Enable registers a webhook subscription on your Strike account the first time it creates an invoice for you, which is why your key needs the `partner.webhooks.manage` scope. Lightning Enable stores the resulting subscription ID and a per-merchant signing secret so it can verify that incoming notifications genuinely came from Strike. :::info Strike Webhooks Are Thin A Strike webhook carries an entity ID, not the payment itself. Lightning Enable calls the Strike API to fetch the full details before acting on it. This is the main behavioral difference from OpenNode, whose webhooks carry the full payload. ::: The endpoint that receives these notifications is `POST /api/webhooks/strike`. It is public by necessity — Strike must reach it — and it authenticates every request by signature rather than by API key. ## Hop 2: Lightning Enable to Your Platform This is the hop you configure. Set a callback URL and Lightning Enable posts signed payment events to it. ### Set Your Callback URL and Secret Set both with the merchant API. Generate the secret yourself (for example, `openssl rand -hex 32`) and keep a copy. Lightning Enable never returns it, and your account's initial secret is not readable, so set your own to verify signatures. ```bash curl -X PUT https://api.lightningenable.com/api/merchant/webhook-url -H "X-API-Key: your-merchant-api-key" -H "Content-Type: application/json" -d '{ "webhookUrl": "https://your-platform.example.com/webhooks/lightning-enable", "webhookSecret": "your-webhook-secret" }' ``` Your endpoint must accept `POST`, be reachable over HTTPS, and return a `2xx` status quickly. Do the slow work after you respond. ### Verify the Signature Every forwarded webhook carries an `X-LightningEnable-Signature` header: ``` X-LightningEnable-Signature: t=1735689600,v1=5257a869e7bcd6742a1f2c... ``` `t` is a Unix timestamp and `v1` is an HMAC-SHA256 signature over `{t}.{raw_body}`. Use the reference verifiers in [Verifying Webhooks](/api-reference/webhooks#verifying-webhooks) ([Node.js](/api-reference/webhooks#nodejs--javascript-verification), [C#](/api-reference/webhooks#c-verification), [Python](/api-reference/webhooks#python-verification)). The common mistakes: - **Enforce timestamp freshness.** Signature validity alone does not stop a replay. Reject requests whose `t` falls outside the tolerance given under [Replay Protection](/api-reference/webhooks#replay-protection). - **Compare in constant time.** Use `CryptographicOperations.FixedTimeEquals` or your language's equivalent. A plain `==` returns on the first mismatched byte and leaks the expected signature to anyone measuring response times. - **Parse the header defensively.** Do not index into `split(',')` positionally. A malformed or hostile header should be rejected, not throw an unhandled exception out of your public endpoint. ### Use the Raw Body Compute the signature over the exact bytes you received. If your framework parses the JSON and you re-serialize it, key order or whitespace can change and the signature will not match. Capture the raw body before parsing. ## Idempotency Retries and network conditions mean the same event can arrive more than once. Key your handler on the payment or invoice identifier and make repeat deliveries a no-op. Do not assume exactly-once delivery. ## Troubleshooting ### No Webhooks Arrive from Strike 1. Confirm your Strike API key has the `partner.webhooks.manage` scope 2. Check the Strike dashboard for a webhook subscription 3. Re-save your Strike key, then create another payment. Saving a key clears the stored subscription, so the next invoice registers a fresh one. ### Webhooks Arrive but Signature Checks Fail 1. Confirm you are hashing the raw body, not a re-serialized version 2. Confirm the signed payload is `{timestamp}.{body}`, not the body alone 3. Confirm you are using the `webhookSecret` you set on `PUT /api/merchant/webhook-url`, not your Strike API key. Your account starts with a generated secret that no endpoint returns, so set your own. 4. Compare the hex encoding — the signature is lowercase hex, not Base64 ### Your Endpoint Times Out Return `2xx` first and process afterward. Slow handlers look like failures and trigger retries. ## Next Steps - [Testing](/strike-setup/testing) — Confirm the full path with a real payment - [Webhooks API Reference](/api-reference/webhooks) — Full event payloads and retry behavior - [API Keys](/strike-setup/api-keys) — Scopes and rotation ============================================================================== # Agent SDKs & Agent Service Agreements Source: https://docs.lightningenable.com/tools/agent-sdks/overview ============================================================================== # Agent SDKs & Agent Service Agreements (ASA) **Agent Service Agreements (ASA)** let AI agents discover each other's paid services, request them, settle via [L402](/products/agentic-commerce/how-it-works) Lightning payments, and publish reputation — all over the open Nostr protocol. Lightning Enable ships three official Agent SDKs implementing ASA: | Language | Package | Install | |----------|---------|---------| | Python (≥3.10) | [`le-agent-sdk` on PyPI](https://pypi.org/project/le-agent-sdk/) | `pip install le-agent-sdk` | | TypeScript | [`le-agent-sdk` on npm](https://www.npmjs.com/package/le-agent-sdk) | `npm install le-agent-sdk` | | .NET 8 or 10 | [`LightningEnable.AgentSdk` on NuGet](https://www.nuget.org/packages/LightningEnable.AgentSdk) | `dotnet add package LightningEnable.AgentSdk` | All three are open source (MIT): [Python](https://github.com/refined-element/le-agent-sdk-python) · [TypeScript](https://github.com/refined-element/le-agent-sdk-ts) · [.NET](https://github.com/refined-element/le-agent-sdk-dotnet) ## The ASA Flow The shipped protocol is deliberately simple — four steps, four Nostr event kinds: ``` 1. DISCOVER Provider publishes a capability (kind 38400). Requester queries relays for capabilities. │ 2. REQUEST Requester publishes a service request (kind 38401) addressed to the provider — one event, stating the capability, a budget in sats, and parameters. │ 3. SETTLE Requester hits the provider's L402 endpoint: 402 challenge → pay Lightning invoice → retry with Authorization: L402 : → result. │ 4. ATTEST Requester publishes an attestation (kind 38403) rating the provider 1–5, tagged to the agreement. ``` :::important No negotiation loop There is **no automated offer / counter-offer / accept negotiation** in ASA. The request (kind 38401) is a single event; settlement terms come from the provider's advertised capability (or its agreement event, kind 38402, which carries the L402 endpoint when it isn't in the capability itself). Payment is proven cryptographically by the L402 preimage — no back-and-forth required. ::: ### Event Kinds | Kind | Event | Published by | Purpose | |------|-------|--------------|---------| | `38400` | Capability | Provider | Advertises a service: service ID (`d` tag), categories, pricing in sats, L402 endpoint. Addressable/replaceable (NIP-33 style) | | `38401` | Service Request | Requester | Requests a capability with a sats budget and key-value params | | `38402` | Agreement | Provider | The provider's response to a request — price and L402 settlement endpoint | | `38403` | Attestation | Requester | Signed 1–5 rating of a counterparty, tagged to the agreement event | ## The Relay The default relay is **`wss://agents.lightningenable.com`** — a [strfry](https://github.com/hoytech/strfry) Nostr relay operated by Lightning Enable that relays kinds 38400–38403. It is a plain relay: it stores and forwards events, nothing more. The SDKs accept any list of relay URLs, so you can run ASA over your own relays. ## Relationship to the MCP Server The [Lightning Enable MCP server](/products/agentic-commerce/mcp-complete-guide) exposes the same protocol as the seven actions of its `agent_services` tool. The old per-action tool names still work as deprecated aliases until MCP v3.0.0. **API-key rules differ:** in the MCP tool, `request`, `publish`, `unpublish`, and `attest` need a `LIGHTNING_ENABLE_API_KEY`; in the SDKs, only producer operations do (see [API Keys: What Needs One](#api-keys-what-needs-one)). | `agent_services` action | Formerly (deprecated alias) | SDK equivalent | |---|---|---| | `discover` | `discover_agent_services` | `discover` | | `request` | `request_agent_service` | `request_service` | | `settle` | `settle_agent_service` | `settle` / `settle_via_l402` | | `publish` | `publish_agent_capability` | `publish_capability` | | `unpublish` | `unpublish_agent_capability` | — (dashboard or MCP only) | | `attest` | `publish_agent_attestation` | `publish_attestation` | | `reputation` | `get_agent_reputation` | `get_reputation_score` / `getReputation` | Use the MCP tools when your agent runs inside an MCP-capable host (Claude, Cursor, etc.); use the SDKs when you're writing agent code directly. ## Attestation & Reputation After settlement, the requester publishes a **kind 38403 attestation**: a Nostr event signed with the requester's key, tagged with the subject's pubkey (`p` tag), the agreement event ID (`e` tag), and a `rating` of 1–5, with free-text review content. Reputation is computed client-side: the SDKs query relays for attestations about a pubkey and average the ratings (Python `get_reputation_score` returns the average or `None`; TypeScript `getReputation` returns `{ average, count, attestations }`; .NET `GetReputationAsync` returns a `ReputationScore`). Because attestations are signed events on open relays, any counterparty can verify them independently — no central reputation database. ## API Keys: What Needs One - **Requester / consumer side** (discover, request, settle, attest, query reputation): **no Lightning Enable API key required.** You need a Nostr private key for signing events and a Lightning wallet (via a pay-invoice callback) to pay invoices. - **Producer / provider side** (`create_challenge`, `verify_payment` — minting and verifying L402 challenges through the [L402 Producer API](/products/agentic-commerce/l402-producer-api)): requires a Lightning Enable merchant API key with an [Agentic Commerce plan](/products/product-overview). As always, Lightning Enable does not hold funds — payments settle wallet-to-wallet over Lightning, and your payment provider facilitates custody and settlement on the producer side. ## Next Steps - [Quickstart](./quickstart.md) — provider and requester examples in all three languages - [L402 Producer API](/products/agentic-commerce/l402-producer-api) — the challenge/verify endpoints behind producer operations - [MCP Complete Guide](/products/agentic-commerce/mcp-complete-guide) — the tool-based route for MCP hosts ============================================================================== # Agent SDK Quickstart Source: https://docs.lightningenable.com/tools/agent-sdks/quickstart ============================================================================== import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; # Agent SDK Quickstart Two roles, one protocol: - **Provider** — advertise a capability (kind 38400), listen for requests (kind 38401), settle via L402. - **Requester** — discover capabilities, then settle directly against the capability's L402 endpoint. Read the [ASA overview](./overview.md) first for the flow and event kinds. ## Install ```bash pip install le-agent-sdk ``` Requires Python 3.10+. ```bash npm install le-agent-sdk ``` ```bash dotnet add package LightningEnable.AgentSdk ``` Targets .NET 8 and .NET 10. You'll need a hex-encoded 32-byte **Nostr private key** for signing events. Producer operations (minting/verifying L402 challenges) additionally need a **Lightning Enable merchant API key** ([Agentic Commerce plan](/products/product-overview)); discovery, requesting, and settling do not. ## Provider: Publish a Capability and Serve Requests ```python import asyncio from le_agent_sdk import AgentManager, AgentCapability, AgentPricing async def main(): manager = AgentManager( private_key="", relay_urls=["wss://agents.lightningenable.com"], le_api_key="", # producer operations only ) # Advertise the service (kind 38400) capability = AgentCapability( service_id="translate-v1", categories=["ai", "translation"], content="AI translation. 50+ languages.", pricing=[AgentPricing(amount=10)], # 10 sats per request l402_endpoint="https://api.example.com/l402/translate", ) event_id = await manager.publish_capability(capability) print(f"Published capability {event_id}") # Listen for incoming service requests (kind 38401) async for request in manager.listen_requests(): print(f"Request from {request.pubkey}: " f"budget={request.budget_sats} sats, params={request.params}") # Respond by publishing an agreement (kind 38402) with your L402 # endpoint via manager.publish_agreement(...), or mint a challenge # directly with manager.create_challenge(...) and verify the # payment with manager.verify_payment(macaroon, preimage). asyncio.run(main()) ``` ```typescript import { AgentManager, AgentCapability, AgentPricing } from "le-agent-sdk"; const manager = new AgentManager({ privateKey: "", relayUrls: ["wss://agents.lightningenable.com"], leApiKey: "", // producer operations only }); // Advertise the service (kind 38400) const capability = new AgentCapability({ serviceId: "translate-v1", categories: ["ai", "translation"], content: "AI translation. 50+ languages.", pricing: [new AgentPricing({ amount: 10 })], // 10 sats per request l402Endpoint: "https://api.example.com/l402/translate", }); const eventId = await manager.publishCapability(capability); console.log(`Published capability ${eventId}`); // Listen for incoming service requests (kind 38401) for await (const request of manager.listenRequests()) { console.log( `Request from ${request.pubkey}: budget=${request.budgetSats} sats`, request.params ); // Respond by publishing an agreement (kind 38402) with your L402 // endpoint via manager.publishAgreement(...), or mint a challenge // directly with manager.createChallenge(...) and verify the payment // with manager.verifyPayment(macaroon, preimage). } ``` ```csharp using LightningEnable.AgentSdk.Agent; using LightningEnable.AgentSdk.Models; await using var manager = new AgentManager(new AgentManagerOptions { PrivateKey = "", RelayUrls = new List { "wss://agents.lightningenable.com" }, LightningEnableApiKey = "", // producer ops only }); await manager.ConnectAsync(); // Advertise the service (kind 38400) var capability = new AgentCapability { DTag = "translate-v1", Name = "Translation Service", Description = "AI translation. 50+ languages.", PriceSats = 10, Endpoint = "https://api.example.com/translate", Categories = new List { "ai", "translation" }, }; var eventId = await manager.PublishCapabilityAsync(capability); Console.WriteLine($"Published capability {eventId}"); // When a request (kind 38401) arrives, mint an L402 challenge for it // and verify the payment once the requester pays: var challenge = await manager.CreateChallengeAsync( agreement, priceSats: 10, description: "Payment for translation"); // ...requester pays the Lightning invoice, presents the preimage: bool valid = await manager.VerifyPaymentAsync(challenge.Macaroon, preimage); ``` :::note The .NET SDK doesn't include a request-listening stream like Python/TypeScript `listen_requests`. Subscribe to kind-38401 events on the relay yourself, or run the provider loop in Python/TypeScript. See the [SimpleProvider example](https://github.com/refined-element/le-agent-sdk-dotnet/tree/main/examples) in the repo. ::: ## Requester: Discover and Settle ```python import asyncio from le_agent_sdk import AgentManager async def pay_invoice(invoice: str) -> str: # Pay the BOLT11 invoice with your Lightning wallet # and return the hex preimage. ... async def main(): manager = AgentManager( private_key="", relay_urls=["wss://agents.lightningenable.com"], pay_invoice_callback=pay_invoice, # enables L402 auto-payment ) # Discover capabilities (kind 38400) capabilities = await manager.discover(categories=["translation"], limit=10) best = capabilities[0] print(f"Using {best.service_id} at {best.pricing[0].amount} sats") # Optionally signal a request (kind 38401) so the provider sees it await manager.request_service( capability_event_id=best.event_id, provider_pubkey=best.pubkey, budget_sats=100, params={"target_lang": "es"}, ) # Settle directly against the capability's L402 endpoint: # 402 challenge -> pay via callback -> retry with preimage response = await manager.settle_via_l402( best, method="POST", json={"text": "Hello", "target_lang": "es"} ) print(response.status_code, response.text) asyncio.run(main()) ``` ```typescript import { AgentManager } from "le-agent-sdk"; const manager = new AgentManager({ privateKey: "", relayUrls: ["wss://agents.lightningenable.com"], // Enables L402 auto-payment: pay the BOLT11 invoice with your // Lightning wallet and return the hex preimage. payInvoiceCallback: async (invoice) => payWithMyWallet(invoice), }); // Discover capabilities (kind 38400) const capabilities = await manager.discover({ categories: ["translation"], limit: 10, }); const best = capabilities[0]; console.log(`Using ${best.serviceId} at ${best.pricing[0]?.amount} sats`); // Optionally signal a request (kind 38401) so the provider sees it await manager.requestService(best.eventId, best.pubkey, 100, { target_lang: "es", }); // Settle directly against the capability's L402 endpoint: // 402 challenge -> pay via callback -> retry with preimage const response = await manager.settleViaL402(best, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ text: "Hello", target_lang: "es" }), }); console.log(response.status, await response.text()); ``` ```csharp using LightningEnable.AgentSdk.Agent; await using var manager = new AgentManager(new AgentManagerOptions { PrivateKey = "", RelayUrls = new List { "wss://agents.lightningenable.com" }, // Enables L402 auto-payment: pay the BOLT11 invoice, return the hex preimage PayInvoiceCallback = (invoice, ct) => PayWithMyWallet(invoice, ct), MaxAmountSats = 1000, // refuse any settlement above this }); await manager.ConnectAsync(); // Discover capabilities (kind 38400) var capabilities = await manager.DiscoverAsync(new DiscoverOptions { Category = "translation", Limit = 10, }); var chosen = capabilities[0]; Console.WriteLine($"Using {chosen.DTag} at {chosen.PriceSats} sats"); // Send a service request (kind 38401) var requestId = await manager.RequestServiceAsync( capabilityId: chosen.Id, budgetSats: 100, parameters: new Dictionary { ["target_lang"] = "es" }); // The provider responds with an agreement (kind 38402) carrying an L402 // endpoint. SettleAsync calls it, pays the 402 challenge through // PayInvoiceCallback, and retries with the proof of payment. var result = await manager.SettleAsync(agreement); Console.WriteLine($"{result.StatusCode}: {result.Content}"); ``` ## After Settlement: Attest Close the loop by publishing a signed 1–5 rating (kind 38403) so other agents can evaluate the provider: ```python await manager.publish_attestation( subject_pubkey=best.pubkey, agreement_id=agreement_event_id, rating=5, content="Fast, accurate translation.", ) score = await manager.get_reputation_score(best.pubkey) # avg 1.0–5.0 or None ``` ```typescript await manager.publishAttestation( best.pubkey, agreementEventId, 5, "Fast, accurate translation." ); const rep = await manager.getReputation(best.pubkey); console.log(rep.average, rep.count); ``` ```csharp await manager.PublishAttestationAsync( subjectPubkey: chosen.Pubkey, agreementId: agreementEventId, rating: 5, content: "Fast, accurate translation."); var reputation = await manager.GetReputationAsync(chosen.Pubkey); ``` ## Next Steps - [ASA Overview](./overview.md) — protocol, event kinds, relay, reputation model - [L402 Producer API](/products/agentic-commerce/l402-producer-api) — the endpoints behind `create_challenge` / `verify_payment` - [HTTP Client Libraries](/tools/l402-requests) — auto-paying L402 clients for arbitrary paid APIs ============================================================================== # AutoGen Integration Source: https://docs.lightningenable.com/tools/autogen ============================================================================== # AutoGen (AG2) Integration Add Lightning payment capability to AutoGen multi-agent conversations. Agents can access L402-protected APIs with automatic micropayments. ## Install ```bash pip install l402-requests[autogen] ``` Set your wallet: ```bash export STRIKE_API_KEY="your-strike-api-key" ``` ## Quick Start ```python from autogen import AssistantAgent, UserProxyAgent from l402_requests.integrations.autogen import register_l402_tools, configure_client from l402_requests import L402Client, BudgetController # Optional: configure budget configure_client(L402Client( budget=BudgetController(max_sats_per_request=500), )) # Agents assistant = AssistantAgent( name="L402Assistant", system_message=( "You can access L402-protected APIs using l402_get and l402_post. " "These tools automatically pay Lightning invoices when needed. " "Check l402_spending_summary to monitor costs. " "Reply TERMINATE when done." ), llm_config={"model": "gpt-4o"}, ) user_proxy = UserProxyAgent( name="UserProxy", human_input_mode="NEVER", is_termination_msg=lambda msg: "TERMINATE" in (msg.get("content") or ""), code_execution_config=False, ) # Register tools register_l402_tools(caller=assistant, executor=user_proxy) # Run user_proxy.initiate_chat( assistant, message="Get the weather forecast for NYC from agent-commerce.store", ) ``` ## Functions ### `register_l402_tools(caller, executor)` Registers all three tools with an AutoGen caller/executor pair: - **`l402_get(url)`** — GET with automatic L402 payment - **`l402_post(url, body)`** — POST with automatic L402 payment - **`l402_spending_summary()`** — Session spending report ### `configure_client(client)` Set a custom `L402Client` before starting the conversation: ```python from l402_requests import L402Client, BudgetController from l402_requests.integrations.autogen import configure_client configure_client(L402Client( budget=BudgetController( max_sats_per_request=1000, max_sats_per_hour=10000, allowed_domains={"agent-commerce.store"}, ), )) ``` ## Wallet Options | Priority | Wallet | Environment Variable | |----------|--------|---------------------| | 1 | LND | `LND_REST_HOST` + `LND_MACAROON_HEX` | | 2 | NWC | `NWC_CONNECTION_STRING` | | 3 | Strike | `STRIKE_API_KEY` | | 4 | OpenNode | `OPENNODE_API_KEY` | ============================================================================== # CrewAI Integration Source: https://docs.lightningenable.com/tools/crewai ============================================================================== # CrewAI Integration Add Lightning payment capability to any CrewAI crew. Agents can access L402-protected APIs with automatic micropayments. ## Install ```bash pip install l402-requests[crewai] ``` Set your wallet: ```bash export STRIKE_API_KEY="your-strike-api-key" ``` ## Quick Start ```python from crewai import Agent, Crew, Task from l402_requests import L402Client, BudgetController from l402_requests.integrations.crewai import L402GetTool, L402PostTool, L402SpendingTool # Shared client for the whole crew client = L402Client( budget=BudgetController(max_sats_per_request=500, max_sats_per_hour=5000), ) # Tools get_tool = L402GetTool(client=client) post_tool = L402PostTool(client=client) spending_tool = L402SpendingTool(client=client) # Agent researcher = Agent( role="API Researcher", goal="Fetch data from L402-gated APIs and summarize findings.", backstory="Expert at accessing Lightning-gated APIs.", tools=[get_tool, spending_tool], ) # Task task = Task( description="Get SEC filing data for Tesla (CIK 1318605) from https://agent-commerce.store/api/edgar/company/1318605", expected_output="Summary of Tesla's recent SEC filings.", agent=researcher, ) # Run crew = Crew(agents=[researcher], tasks=[task]) result = crew.kickoff() ``` ## Tools ### L402GetTool HTTP GET with automatic L402 payment. - **Input:** `url` (str) - **Returns:** Response body as text ### L402PostTool HTTP POST with automatic L402 payment. - **Input:** `url` (str), `body` (optional JSON string) - **Returns:** Response body as text ### L402SpendingTool Session spending summary. - **Input:** None - **Returns:** JSON with total sats, per-domain breakdown ## Shared Client Always pass the same `L402Client` to all tools. This ensures: - **Credential cache** is shared (no double-payments) - **Budget** is enforced across all tools as one limit - **Spending log** is unified ============================================================================== # L402-Requests (.NET) Source: https://docs.lightningenable.com/tools/l402-dotnet ============================================================================== # L402-Requests (.NET) **Three lines of C#. Paid APIs just work.** ```csharp using L402Requests; using var client = new L402HttpClient(); var response = await client.GetAsync("https://api.example.com/paid-resource"); Console.WriteLine(await response.Content.ReadAsStringAsync()); ``` That's the entire integration. No payment logic. No invoice parsing. No retry code. No protocol knowledge required. Behind the scenes, `L402Requests` detects the 402 challenge, pays the Lightning invoice from your wallet, caches the credential, and retries the request. You get back a normal `HttpResponseMessage`. The API just worked — and it got paid. ## Install ```bash dotnet add package L402Requests ``` Set one environment variable for your wallet and you're done: ```bash export STRIKE_API_KEY="your-strike-api-key" ``` That's it. Every L402-protected API you call will automatically get paid. ## How It Works You never see any of this — it happens automatically: ``` Your Code L402Requests L402 Server Lightning ──────── ──────────── ─────────── ───────── │ │ │ │ │── GetAsync("/data") ────▶│ │ │ │ │──── GET /data ──────────▶│ │ │ │◀── 402 + invoice + mac ──│ │ │ │ │ │ │ │ check budget │ │ │ │ extract amount │ │ │ │ │ │ │ │──── pay invoice ────────────────────────────── ▶│ │ │◀─── preimage ───────────────────────────────── │ │ │ │ │ │ │── GET /data ────────────▶│ │ │ │ Authorization: L402 │ │ │ │◀──── 200 + data ────────│ │ │◀── 200 + data ──────────│ │ │ ``` 1. You make an HTTP request — `client.GetAsync(url)` 2. If the server returns **200**, the response comes back as-is 3. If the server returns **402** with an L402 challenge: - The invoice is parsed automatically - The amount is checked against your budget - The invoice is paid via your Lightning wallet - The request is retried with `Authorization: L402 {macaroon}:{preimage}` 4. Credentials are cached — subsequent requests to the same endpoint don't re-pay ## Wallet Configuration Set environment variables for your wallet. The library auto-detects in priority order: | Priority | Wallet | Environment Variables | Preimage | Notes | |----------|--------|-----------------------|----------|-------| | 1 | LND | `LND_REST_HOST` + `LND_MACAROON_HEX` | Yes | Requires running a node | | 2 | NWC | `NWC_CONNECTION_STRING` | Yes | CoinOS, CLINK compatible | | 3 | Strike | `STRIKE_API_KEY` | Yes | No infrastructure required | | 4 | OpenNode | `OPENNODE_API_KEY` | Limited | No preimage support | :::tip Recommended: Strike Strike has full preimage support and requires no infrastructure. Set `STRIKE_API_KEY` and you're done. ::: :::note Credential resolution & priority override If an environment variable is empty or a `${...}` placeholder, the client falls back to `~/.lightning-enable/config.json` (shared with the Lightning Enable MCP server), and a `wallets.priority` key in that file can reorder detection. To switch wallets, clear the old credential in **both** places — otherwise the config-file fallback silently re-enables it. ::: ### Strike ```bash export STRIKE_API_KEY="your-strike-api-key" ``` ### LND ```bash export LND_REST_HOST="https://localhost:8080" export LND_MACAROON_HEX="your-admin-macaroon-hex" export LND_TLS_CERT_PATH="/path/to/tls.cert" # optional ``` ### NWC (Nostr Wallet Connect) ```bash export NWC_CONNECTION_STRING="nostr+walletconnect://pubkey?relay=wss://relay&secret=hex" ``` ### OpenNode ```bash export OPENNODE_API_KEY="your-opennode-key" ``` :::warning OpenNode L402 Limitation OpenNode does not return payment preimages, which means L402 credential construction will fail. For L402 use cases, use Strike, LND, or a compatible NWC wallet. ::: ### L402 Wallet Compatibility L402 requires the payment **preimage** (proof of payment) to construct credentials. Not all wallets return it. If yours doesn't, payment succeeds but API access fails. | Wallet | Returns Preimage | L402 Works | Notes | |--------|-----------------|------------|-------| | **LND** | Yes | Yes | Requires running a node | | **NWC (CoinOS)** | Yes | Yes | Free, easy setup | | **NWC (CLINK)** | Yes | Yes | Nostr users | | **Strike** | Yes | Yes | Easy setup, no infrastructure | | **Alby Hub** | Yes | Yes | Run your own hub, NWC compatible | | **Primal** | No | No | Direct payments only | | **OpenNode** | No | No | Direct payments only | For detailed wallet setup instructions, see the [MCP Wallet Setup Guide](/products/agentic-commerce/mcp-wallet-setup). ### Explicit Wallet You can also pass a wallet directly instead of relying on auto-detection: ```csharp using L402Requests; using L402Requests.Wallets; using var client = new L402HttpClient(new StrikeWallet("your-api-key")); var response = await client.GetAsync("https://api.example.com/paid-resource"); ``` ## Budget Controls Safety is built in. Budgets are enabled by default so you can't accidentally overspend: ```csharp using var client = new L402HttpClient(new L402Options { MaxSatsPerRequest = 500, // Max per single payment (default: 1,000) MaxSatsPerHour = 5000, // Hourly rolling limit (default: 10,000) MaxSatsPerDay = 25000, // Daily rolling limit (default: 50,000) AllowedDomains = ["api.example.com"], // Optional domain allowlist }); ``` If a payment would exceed any limit, `BudgetExceededException` is raised **before** the payment is attempted — no sats leave your wallet. To disable budgets entirely: ```csharp using var client = new L402HttpClient(new L402Options { BudgetEnabled = false }); // Not recommended ``` ### Default Limits | Limit | Default | Description | |-------|---------|-------------| | `MaxSatsPerRequest` | 1,000 sats | Rejects any single invoice above this | | `MaxSatsPerHour` | 10,000 sats | Rolling 1-hour window | | `MaxSatsPerDay` | 50,000 sats | Rolling 24-hour window | ### Domain Allowlist Restrict payments to specific domains: ```csharp using var client = new L402HttpClient(new L402Options { AllowedDomains = ["api.example.com", "store.lightningenable.com"], }); ``` Any request to a domain not in the list will raise `DomainNotAllowedException` before attempting payment. ## DI / HttpClientFactory For ASP.NET Core and services using dependency injection: ```csharp // In Program.cs builder.Services.AddL402HttpClient("myapi", options => { options.MaxSatsPerRequest = 500; options.MaxSatsPerHour = 5000; options.AllowedDomains = ["api.example.com"]; }); // In consuming class public class MyService(IHttpClientFactory factory) { public async Task GetPaidData() { var client = factory.CreateClient("myapi"); var response = await client.GetAsync("https://api.example.com/paid-resource"); return await response.Content.ReadAsStringAsync(); } } ``` ## Spending Introspection Track every payment made during a session: ```csharp using var client = new L402HttpClient(); await client.GetAsync("https://api.example.com/data"); await client.GetAsync("https://api.example.com/more-data"); // Inspect spending Console.WriteLine($"Total: {client.SpendingLog.TotalSpent()} sats"); Console.WriteLine($"Last hour: {client.SpendingLog.SpentLastHour()} sats"); Console.WriteLine($"Today: {client.SpendingLog.SpentToday()} sats"); Console.WriteLine($"By domain: {string.Join(", ", client.SpendingLog.ByDomain())}"); // Export as JSON for auditing Console.WriteLine(client.SpendingLog.ToJson()); ``` ## Credential Caching L402 credentials are cached by `(domain, path_prefix)` so you don't re-pay for the same endpoint within a session. The cache uses an LRU eviction strategy with a default TTL of 1 hour. ```csharp using var client = new L402HttpClient(new L402Options { CacheMaxSize = 256, // Maximum cached credentials CacheTtlSeconds = 3600.0, // 1 hour TTL }); ``` ## Error Handling ```csharp using L402Requests; using var client = new L402HttpClient(); try { var response = await client.GetAsync("https://api.example.com/paid-resource"); } catch (BudgetExceededException e) { Console.WriteLine($"Over budget: {e.LimitType} limit is {e.LimitSats} sats"); } catch (PaymentFailedException e) { Console.WriteLine($"Payment failed: {e.Reason}"); } catch (NoWalletException) { Console.WriteLine("No wallet configured — set STRIKE_API_KEY or other wallet env vars"); } ``` | Exception | When | |-----------|------| | `BudgetExceededException` | Payment would exceed a budget limit | | `PaymentFailedException` | Lightning payment failed (routing, timeout, etc.) | | `InvoiceExpiredException` | Invoice expired before payment | | `NoWalletException` | No wallet env vars detected | | `DomainNotAllowedException` | Domain not in `AllowedDomains` | | `ChallengeParseException` | Malformed L402 challenge header | ## Example: Lightning Enable Store Access the [Lightning Enable Store](https://store.lightningenable.com) — a live L402 commerce demo. :::warning Budget Configuration Required Store products cost **25,000 - 45,000+ sats** (including shipping). The default budget (1,000 sats per request, 10,000 per hour) rejects these payments, so raise the per-request, hourly, and daily limits before purchasing. ::: :::info US Shipping Only The Lightning Enable Store currently ships to **US addresses only**. ::: The store uses a **two-step L402 flow** for physical goods: checkout returns a 402 with an invoice, and after payment you claim the order at a separate `/claim` endpoint with the same L402 credential. ```csharp using System.Net.Http.Json; using L402Requests; // Raise the hourly and daily caps too, or the 10,000-sat hourly default rejects the purchase using var client = new L402HttpClient(new L402Options { MaxSatsPerRequest = 50000, MaxSatsPerHour = 50000, MaxSatsPerDay = 100000, }); // Step 1: Browse the catalog (free) var catalog = await client.GetAsync("https://store.lightningenable.com/api/store/catalog"); // Step 2: Checkout; the client pays the 402 automatically await client.PostAsync("https://store.lightningenable.com/api/store/checkout", JsonContent.Create(new { items = new[] { new { productId = 2, quantity = 1, size = "L", color = "Black" } } })); // Step 3: Claim with the credential from the spending log (header only, no body) var record = client.SpendingLog.Records[^1]; using var plainHttp = new HttpClient(); using var claimRequest = new HttpRequestMessage( HttpMethod.Post, "https://store.lightningenable.com/api/store/claim"); claimRequest.Headers.TryAddWithoutValidation( "Authorization", $"L402 {record.Macaroon}:{record.Preimage}"); var claim = await plainHttp.SendAsync(claimRequest); Console.WriteLine(await claim.Content.ReadAsStringAsync()); // contains claimUrl ``` Claim tokens expire 30 days after payment. ## API Reference ### `L402HttpClient` ```csharp new L402HttpClient() // Auto-detect wallet, default options new L402HttpClient(IWallet wallet) // Explicit wallet new L402HttpClient(L402Options options) // Custom options new L402HttpClient(IWallet? wallet, L402Options? options) // Both ``` Methods: `.GetAsync()`, `.PostAsync()`, `.PutAsync()`, `.PatchAsync()`, `.DeleteAsync()`, `.SendAsync()` Properties: - `.SpendingLog` — `SpendingLog` instance for payment history ### `L402Options` ```csharp new L402Options { MaxSatsPerRequest = 1000, // Default: 1000 MaxSatsPerHour = 10000, // Default: 10000 MaxSatsPerDay = 50000, // Default: 50000 AllowedDomains = null, // null = all domains Wallet = null, // null = auto-detect BudgetEnabled = true, // false to disable all limits CacheMaxSize = 256, // LRU cache size CacheTtlSeconds = 3600.0, // null to disable expiration } ``` ### `ServiceCollectionExtensions` ```csharp services.AddL402HttpClient("name", options => { ... }); services.AddL402HttpClient(options => { ... }); ``` ### Wallet Classes - `StrikeWallet(string apiKey, string? baseUrl = null)` - `LndWallet(string host, string macaroonHex, string? tlsCertPath = null, bool insecure = false)` - `NwcWallet(string connectionString, TimeSpan? timeout = null, string? encryption = null)` - `OpenNodeWallet(string apiKey, string? baseUrl = null)` ## Also Available - **Python**: [`l402-requests`](/tools/l402-requests) — same "three lines of code" experience for Python - **TypeScript**: [`l402-requests`](/tools/l402-ts) — same "three lines of code" experience for TypeScript/Node.js ## Source Code [GitHub Repository](https://github.com/refined-element/l402-dotnet) (MIT License) ============================================================================== # L402-Requests (Python) Source: https://docs.lightningenable.com/tools/l402-requests ============================================================================== # L402-Requests **Three lines of Python. Paid APIs just work.** ```python import l402_requests response = l402_requests.get("https://api.example.com/paid-resource") print(response.json()) ``` That's the entire integration. No payment logic. No invoice parsing. No retry code. No protocol knowledge required. Behind the scenes, `L402-Requests` detects the 402 challenge, pays the Lightning invoice from your wallet, caches the credential, and retries the request. You get back a normal `httpx.Response`. The API just worked — and it got paid. ## Install ```bash pip install l402-requests ``` Set one environment variable for your wallet and you're done: ```bash export STRIKE_API_KEY="your-strike-api-key" ``` That's it. Every L402-protected API you call will automatically get paid. ## How It Works You never see any of this — it happens automatically: ``` Your Code L402-Requests L402 Server Lightning ──────── ───────────── ─────────── ───────── │ │ │ │ │──── GET /resource ──────▶│ │ │ │ │──── GET /resource ──────▶│ │ │ │◀── 402 + invoice + mac ──│ │ │ │ │ │ │ │ check budget │ │ │ │ extract amount │ │ │ │ │ │ │ │──── pay invoice ────────────────────────────── ▶│ │ │◀─── preimage ───────────────────────────────── │ │ │ │ │ │ │── GET /resource ────────▶│ │ │ │ Authorization: L402 │ │ │ │◀──── 200 + data ────────│ │ │◀── 200 + data ──────────│ │ │ ``` 1. You make an HTTP request — `l402_requests.get(url)` 2. If the server returns **200**, the response comes back as-is 3. If the server returns **402** with an L402 challenge: - The invoice is parsed automatically - The amount is checked against your budget - The invoice is paid via your Lightning wallet - The request is retried with `Authorization: L402 {macaroon}:{preimage}` 4. Credentials are cached — subsequent requests to the same endpoint don't re-pay ## Wallet Configuration Set environment variables for your wallet. The library auto-detects in priority order: | Priority | Wallet | Environment Variables | Preimage | Notes | |----------|--------|-----------------------|----------|-------| | 1 | LND | `LND_REST_HOST` + `LND_MACAROON_HEX` | Yes | Requires running a node | | 2 | NWC | `NWC_CONNECTION_STRING` | Yes | CoinOS, CLINK compatible | | 3 | Strike | `STRIKE_API_KEY` | Yes | No infrastructure required | | 4 | OpenNode | `OPENNODE_API_KEY` | Limited | No preimage support | :::tip Recommended: Strike Strike has full preimage support and requires no infrastructure. Set `STRIKE_API_KEY` and you're done. ::: :::note Credential resolution & priority override If an environment variable is empty or a `${...}` placeholder, the client falls back to `~/.lightning-enable/config.json` (shared with the Lightning Enable MCP server), and a `wallets.priority` key in that file can reorder detection. To switch wallets, clear the old credential in **both** places — otherwise the config-file fallback silently re-enables it. ::: ### Strike ```bash export STRIKE_API_KEY="your-strike-api-key" ``` ### LND ```bash export LND_REST_HOST="https://localhost:8080" export LND_MACAROON_HEX="your-admin-macaroon-hex" export LND_TLS_CERT_PATH="/path/to/tls.cert" # optional ``` ### NWC (Nostr Wallet Connect) NWC support ships in the base install (since 0.3.0 — the old `[nwc]` extra is an empty no-op kept for compatibility): ```bash export NWC_CONNECTION_STRING="nostr+walletconnect://pubkey?relay=wss://relay&secret=hex" ``` ### OpenNode ```bash export OPENNODE_API_KEY="your-opennode-key" ``` :::warning OpenNode L402 Limitation OpenNode does not return payment preimages, which means L402 credential construction will fail. For L402 use cases, use Strike, LND, or a compatible NWC wallet. ::: ### L402 Wallet Compatibility L402 requires the payment **preimage** (proof of payment) to construct credentials. Not all wallets return it. If yours doesn't, payment succeeds but API access fails. | Wallet | Returns Preimage | L402 Works | Notes | |--------|-----------------|------------|-------| | **LND** | ✅ Always | ✅ Yes | Requires running a node | | **NWC (CoinOS)** | ✅ Yes | ✅ Yes | Free, easy setup | | **NWC (CLINK)** | ✅ Yes | ✅ Yes | Nostr users | | **Strike** | ✅ Yes | ✅ Yes | Easy setup, no infrastructure | | **Alby Hub** | ✅ Yes | ✅ Yes | Run your own hub, NWC compatible | | **Primal** | ❌ No | ❌ No | Direct payments only | | **OpenNode** | ❌ No | ❌ No | Direct payments only | For detailed wallet setup instructions, see the [MCP Wallet Setup Guide](/products/agentic-commerce/mcp-wallet-setup). ### Explicit Wallet You can also pass a wallet directly instead of relying on auto-detection: ```python from l402_requests import L402Client, StrikeWallet client = L402Client( wallet=StrikeWallet(api_key="your-key"), ) response = client.get("https://api.example.com/paid-resource") ``` ## Budget Controls Safety is built in. Budgets are enabled by default so you can't accidentally overspend: ```python from l402_requests import L402Client, BudgetController client = L402Client( budget=BudgetController( max_sats_per_request=500, # Max per single payment (default: 1,000) max_sats_per_hour=5000, # Hourly rolling limit (default: 10,000) max_sats_per_day=25000, # Daily rolling limit (default: 50,000) allowed_domains={"api.example.com"}, # Optional domain allowlist ) ) ``` If a payment would exceed any limit, `BudgetExceededError` is raised **before** the payment is attempted — no sats leave your wallet. To disable budgets entirely: ```python client = L402Client(budget=None) # Not recommended ``` ### Default Limits | Limit | Default | Description | |-------|---------|-------------| | `max_sats_per_request` | 1,000 sats | Rejects any single invoice above this | | `max_sats_per_hour` | 10,000 sats | Rolling 1-hour window | | `max_sats_per_day` | 50,000 sats | Rolling 24-hour window | ### Domain Allowlist Restrict payments to specific domains: ```python budget = BudgetController( allowed_domains={"api.example.com", "store.lightningenable.com"} ) ``` Any request to a domain not in the list will raise `DomainNotAllowedError` before attempting payment. ## Async Support Full async support via `AsyncL402Client`: ```python from l402_requests import AsyncL402Client async with AsyncL402Client() as client: response = await client.get("https://api.example.com/paid-resource") print(response.json()) ``` ## Spending Introspection Track every payment made during a session: ```python from l402_requests import L402Client client = L402Client() client.get("https://api.example.com/data") client.get("https://api.example.com/more-data") # Inspect spending print(f"Total: {client.spending_log.total_spent()} sats") print(f"Last hour: {client.spending_log.spent_last_hour()} sats") print(f"Today: {client.spending_log.spent_today()} sats") print(f"By domain: {client.spending_log.by_domain()}") # Export as JSON for auditing print(client.spending_log.to_json()) ``` ## Credential Caching L402 credentials are cached by `(domain, path_prefix)` so you don't re-pay for the same endpoint within a session. The cache uses an LRU eviction strategy with a default TTL of 1 hour. ```python from l402_requests import L402Client, CredentialCache client = L402Client( credential_cache=CredentialCache( max_size=256, # Maximum cached credentials default_ttl=3600.0, # 1 hour TTL ) ) ``` ## Error Handling ```python from l402_requests import L402Client, BudgetExceededError, PaymentFailedError, NoWalletError client = L402Client() try: response = client.get("https://api.example.com/paid-resource") except BudgetExceededError as e: print(f"Over budget: {e.limit_type} limit is {e.limit_sats} sats") except PaymentFailedError as e: print(f"Payment failed: {e.reason}") except NoWalletError: print("No wallet configured — set STRIKE_API_KEY or other wallet env vars") ``` | Exception | When | |-----------|------| | `BudgetExceededError` | Payment would exceed a budget limit | | `PaymentFailedError` | Lightning payment failed (routing, timeout, etc.) | | `InvoiceExpiredError` | Invoice expired before payment | | `NoWalletError` | No wallet env vars detected | | `DomainNotAllowedError` | Domain not in `allowed_domains` | | `ChallengeParseError` | Malformed L402 challenge header | ## Example: Lightning Enable Store Access the [Lightning Enable Store](https://store.lightningenable.com) — a live L402 commerce demo. :::warning Budget Configuration Required Store products cost **25,000 - 45,000+ sats** (including shipping). The default budget (1,000 sats per request, 10,000 per hour) rejects these payments, so raise the per-request, hourly, and daily limits before purchasing. ::: :::info US Shipping Only The Lightning Enable Store currently ships to **US addresses only**. ::: The store uses a **two-step L402 flow** for physical goods: checkout returns a 402 with an invoice, and after payment you claim the order at a separate `/claim` endpoint with the same L402 credential. The claim URL can go to a gift recipient, who enters their own shipping address. ```python import httpx from l402_requests import L402Client, BudgetController # Raise the hourly and daily caps too, or the 10,000-sat hourly default rejects the purchase client = L402Client(budget=BudgetController( max_sats_per_request=50000, max_sats_per_hour=50000, max_sats_per_day=100000)) # Step 1: Browse the catalog (free) catalog = client.get("https://store.lightningenable.com/api/store/catalog") # Step 2: Checkout; the client pays the 402 automatically client.post( "https://store.lightningenable.com/api/store/checkout", json={"items": [{"productId": 2, "quantity": 1, "size": "L", "color": "Black"}]}, ) # Step 3: Claim with the credential from the spending log (header only, no body) record = client.spending_log.records[-1] claim = httpx.post( "https://store.lightningenable.com/api/store/claim", headers={"Authorization": f"L402 {record.macaroon}:{record.preimage}"}, ) print(f"Claim URL: {claim.json()['claimUrl']}") # share with whoever enters shipping ``` Claim tokens expire 30 days after payment. ## API Reference ### `L402Client` ```python L402Client( wallet: WalletBase | None = None, budget: BudgetController | None = , credential_cache: CredentialCache | None = None, **httpx_kwargs, ) ``` Methods: `.get()`, `.post()`, `.put()`, `.delete()`, `.patch()`, `.head()`, `.options()`, `.request()` Properties: - `.spending_log` — `SpendingLog` instance for payment history ### `AsyncL402Client` Same API as `L402Client` but all methods are `async`. Use as async context manager: ```python async with AsyncL402Client() as client: response = await client.get(url) ``` ### `BudgetController` ```python BudgetController( max_sats_per_request: int = 1000, max_sats_per_hour: int = 10000, max_sats_per_day: int = 50000, allowed_domains: set[str] | None = None, ) ``` ### Wallet Classes - `StrikeWallet(api_key: str, base_url: str | None = None)` - `LndWallet(host: str, macaroon_hex: str, tls_cert_path: str | None = None)` - `NwcWallet(connection_string: str, timeout: float = 60.0)` - `OpenNodeWallet(api_key: str, base_url: str | None = None)` ## Also Available - **TypeScript**: [`l402-requests`](/tools/l402-ts) — same "three lines of code" experience for TypeScript/Node.js - **.NET**: [`L402Requests`](/tools/l402-dotnet) — same "three lines of code" experience for .NET ## Source Code [GitHub Repository](https://github.com/refined-element/l402-requests) (MIT License) ============================================================================== # L402-Requests (TypeScript) Source: https://docs.lightningenable.com/tools/l402-ts ============================================================================== # L402-Requests (TypeScript) **Three lines of TypeScript. Paid APIs just work.** ```typescript import { get } from 'l402-requests'; const response = await get("https://api.example.com/paid-resource"); console.log(await response.json()); ``` That's the entire integration. No payment logic. No invoice parsing. No retry code. No protocol knowledge required. Behind the scenes, `l402-requests` detects the 402 challenge, pays the Lightning invoice from your wallet, caches the credential, and retries the request. You get back a normal `Response`. The API just worked — and it got paid. ## Install ```bash npm install l402-requests ``` Set one environment variable for your wallet and you're done: ```bash export STRIKE_API_KEY="your-strike-api-key" ``` That's it. Every L402-protected API you call will automatically get paid. ## How It Works You never see any of this — it happens automatically: ``` Your Code l402-requests L402 Server Lightning ───────── ───────────── ─────────── ───────── │ │ │ │ │── GET /resource ──▶│ │ │ │ │── GET /resource ───────▶│ │ │ │◀── 402 + invoice + mac ─│ │ │ │ │ │ │ │ check budget │ │ │ │ extract amount │ │ │ │ │ │ │ │── pay invoice ──────────────────────────────▶│ │ │◀── preimage ────────────────────────────────│ │ │ │ │ │ │── GET /resource ────────▶│ │ │ │ Authorization: L402 │ │ │ │◀──── 200 + data ────────│ │ │◀── 200 + data ─────│ │ │ ``` 1. You make an HTTP request — `get(url)` 2. If the server returns **200**, the response comes back as-is 3. If the server returns **402** with an L402 challenge: - The invoice is parsed automatically - The amount is checked against your budget - The invoice is paid via your Lightning wallet - The request is retried with `Authorization: L402 {macaroon}:{preimage}` 4. Credentials are cached — subsequent requests to the same endpoint don't re-pay ## Wallet Configuration Set environment variables for your wallet. The library auto-detects in priority order: | Priority | Wallet | Environment Variables | Preimage | Notes | |----------|--------|-----------------------|----------|-------| | 1 | LND | `LND_REST_HOST` + `LND_MACAROON_HEX` | Yes | Requires running a node | | 2 | NWC | `NWC_CONNECTION_STRING` | Yes | CoinOS, CLINK compatible | | 3 | Strike | `STRIKE_API_KEY` | Yes | No infrastructure required | | 4 | OpenNode | `OPENNODE_API_KEY` | Limited | No preimage support | :::tip Recommended: Strike Strike has full preimage support and requires no infrastructure. Set `STRIKE_API_KEY` and you're done. ::: :::note Credential resolution & priority override If an environment variable is empty or a `${...}` placeholder, the client falls back to `~/.lightning-enable/config.json` (shared with the Lightning Enable MCP server), and a `wallets.priority` key in that file can reorder detection. To switch wallets, clear the old credential in **both** places — otherwise the config-file fallback silently re-enables it. ::: ### Strike ```bash export STRIKE_API_KEY="your-strike-api-key" ``` ### LND ```bash export LND_REST_HOST="https://localhost:8080" export LND_MACAROON_HEX="your-admin-macaroon-hex" ``` ### NWC (Nostr Wallet Connect) NWC requires optional peer dependencies: ```bash npm install @noble/secp256k1 ws export NWC_CONNECTION_STRING="nostr+walletconnect://pubkey?relay=wss://relay&secret=hex" ``` ### OpenNode ```bash export OPENNODE_API_KEY="your-opennode-key" ``` :::warning OpenNode L402 Limitation OpenNode does not return payment preimages, which means L402 credential construction will fail. For L402 use cases, use Strike, LND, or a compatible NWC wallet. ::: ### L402 Wallet Compatibility L402 requires the payment **preimage** (proof of payment) to construct credentials. Not all wallets return it. If yours doesn't, payment succeeds but API access fails. | Wallet | Returns Preimage | L402 Works | Notes | |--------|-----------------|------------|-------| | **LND** | Yes | Yes | Requires running a node | | **NWC (CoinOS)** | Yes | Yes | Free, easy setup | | **NWC (CLINK)** | Yes | Yes | Nostr users | | **Strike** | Yes | Yes | Easy setup, no infrastructure | | **Alby Hub** | Yes | Yes | Run your own hub, NWC compatible | | **Primal** | No | No | Direct payments only | | **OpenNode** | No | No | Direct payments only | For detailed wallet setup instructions, see the [MCP Wallet Setup Guide](/products/agentic-commerce/mcp-wallet-setup). ### Explicit Wallet You can also pass a wallet directly instead of relying on auto-detection: ```typescript import { L402Client, StrikeWallet } from 'l402-requests'; const client = new L402Client({ wallet: new StrikeWallet("your-key"), }); const response = await client.get("https://api.example.com/paid-resource"); ``` ## Budget Controls Safety is built in. Budgets are enabled by default so you can't accidentally overspend: ```typescript import { L402Client, BudgetController } from 'l402-requests'; const client = new L402Client({ budget: new BudgetController({ maxSatsPerRequest: 500, // Max per single payment (default: 1,000) maxSatsPerHour: 5000, // Hourly rolling limit (default: 10,000) maxSatsPerDay: 25000, // Daily rolling limit (default: 50,000) allowedDomains: new Set(["api.example.com"]), }), }); ``` If a payment would exceed any limit, `BudgetExceededError` is thrown **before** the payment is attempted — no sats leave your wallet. To disable budgets entirely: ```typescript const client = new L402Client({ budget: null }); // Not recommended ``` ### Default Limits | Limit | Default | Description | |-------|---------|-------------| | `maxSatsPerRequest` | 1,000 sats | Rejects any single invoice above this | | `maxSatsPerHour` | 10,000 sats | Rolling 1-hour window | | `maxSatsPerDay` | 50,000 sats | Rolling 24-hour window | ### Domain Allowlist Restrict payments to specific domains: ```typescript const budget = new BudgetController({ allowedDomains: new Set(["api.example.com", "store.lightningenable.com"]), }); ``` Any request to a domain not in the list will raise `DomainNotAllowedError` before attempting payment. ## Spending Introspection Track every payment made during a session: ```typescript import { L402Client } from 'l402-requests'; const client = new L402Client(); await client.get("https://api.example.com/data"); await client.get("https://api.example.com/more-data"); // Inspect spending console.log(`Total: ${client.spendingLog.totalSpent()} sats`); console.log(`Last hour: ${client.spendingLog.spentLastHour()} sats`); console.log(`Today: ${client.spendingLog.spentToday()} sats`); console.log(`By domain:`, client.spendingLog.byDomain()); // Export as JSON for auditing console.log(client.spendingLog.toJSON()); ``` ## Credential Caching L402 credentials are cached by `(domain, path_prefix)` so you don't re-pay for the same endpoint within a session. The cache uses an LRU eviction strategy with a default TTL of 1 hour. ```typescript import { L402Client, CredentialCache } from 'l402-requests'; const client = new L402Client({ credentialCache: new CredentialCache({ maxSize: 256, // Maximum cached credentials defaultTtlMs: 3_600_000, // 1 hour TTL }), }); ``` ## Error Handling ```typescript import { L402Client, BudgetExceededError, PaymentFailedError, NoWalletError } from 'l402-requests'; const client = new L402Client(); try { const response = await client.get("https://api.example.com/paid-resource"); } catch (e) { if (e instanceof BudgetExceededError) { console.log(`Over budget: ${e.limitType} limit is ${e.limitSats} sats`); } else if (e instanceof PaymentFailedError) { console.log(`Payment failed: ${e.reason}`); } else if (e instanceof NoWalletError) { console.log("No wallet configured — set STRIKE_API_KEY or other wallet env vars"); } } ``` | Exception | When | |-----------|------| | `BudgetExceededError` | Payment would exceed a budget limit | | `PaymentFailedError` | Lightning payment failed (routing, timeout, etc.) | | `InvoiceExpiredError` | Invoice expired before payment | | `NoWalletError` | No wallet env vars detected | | `DomainNotAllowedError` | Domain not in `allowedDomains` | | `ChallengeParseError` | Malformed L402 challenge header | ## Example: Lightning Enable Store Access the [Lightning Enable Store](https://store.lightningenable.com) — a live L402 commerce demo. :::warning Budget Configuration Required Store products cost **25,000 - 45,000+ sats** (including shipping). The default budget (1,000 sats per request, 10,000 per hour) rejects these payments, so raise the per-request, hourly, and daily limits before purchasing. ::: The store uses a **two-step L402 flow** for physical goods: checkout returns a 402 with an invoice, and after payment you claim the order at a separate `/claim` endpoint with the same L402 credential. ```typescript import { L402Client, BudgetController } from 'l402-requests'; // Raise the hourly and daily caps too, or the 10,000-sat hourly default rejects the purchase const client = new L402Client({ budget: new BudgetController({ maxSatsPerRequest: 50000, maxSatsPerHour: 50000, maxSatsPerDay: 100000 }), }); // Step 1: Browse the catalog (free) const catalog = await client.get("https://store.lightningenable.com/api/store/catalog"); // Step 2: Checkout; the client pays the 402 automatically await client.post("https://store.lightningenable.com/api/store/checkout", { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ items: [{ productId: 2, quantity: 1, size: "L", color: "Black" }] }), }); // Step 3: Claim with the credential from the spending log (header only, no body) const record = client.spendingLog.records.at(-1)!; const claim = await fetch("https://store.lightningenable.com/api/store/claim", { method: "POST", headers: { "Authorization": `L402 ${record.macaroon}:${record.preimage}` }, }); console.log(`Claim URL: ${(await claim.json()).claimUrl}`); ``` Claim tokens expire 30 days after payment. ## API Reference ### `L402Client` ```typescript new L402Client({ wallet?: Wallet, budget?: BudgetController | null, // undefined = default, null = disabled credentialCache?: CredentialCache, fetchOptions?: RequestInit, }) ``` Methods: `.get()`, `.post()`, `.put()`, `.delete()`, `.patch()`, `.head()`, `.fetch()` Properties: - `.spendingLog` — `SpendingLog` instance for payment history ### Module-Level Convenience Functions ```typescript import { get, post, put, del, patch } from 'l402-requests'; ``` Uses a lazy singleton `L402Client` with default options. ### `BudgetController` ```typescript new BudgetController({ maxSatsPerRequest?: number, // default: 1000 maxSatsPerHour?: number, // default: 10000 maxSatsPerDay?: number, // default: 50000 allowedDomains?: Set, // default: null (all domains) }) ``` ### Wallet Classes - `StrikeWallet(apiKey: string, baseUrl?: string)` - `LndWallet(host: string, macaroonHex: string)` - `NwcWallet(connectionString: string, timeout?: number)` (milliseconds, default 60,000) - `OpenNodeWallet(apiKey: string, baseUrl?: string)` ## Zero Dependencies The core library has **zero required dependencies**. It uses Node.js 18+ built-in `fetch()` for HTTP requests. Only the NWC wallet adapter requires optional peer dependencies (`@noble/secp256k1` and `ws`). ## Also Available - **Python**: [`l402-requests`](/tools/l402-requests) — same "three lines of code" experience for Python - **.NET**: [`L402Requests`](/tools/l402-dotnet) — same experience for .NET ## Source Code [GitHub Repository](https://github.com/refined-element/l402-ts) (MIT License) ============================================================================== # LangChain Integration Source: https://docs.lightningenable.com/tools/langchain ============================================================================== # LangChain Integration Add Lightning payment capability to any LangChain agent. Your agent can access L402-protected APIs with automatic micropayments. ## Install ```bash pip install l402-requests[langchain] langchain-openai langgraph ``` The `[langchain]` extra installs `langchain-core`. `langchain-openai` and `langgraph` are only for the Quick Start below; swap in your own model provider and agent runtime. Set your wallet: ```bash export STRIKE_API_KEY="your-strike-api-key" ``` ## Quick Start ```python from langchain_openai import ChatOpenAI from langgraph.prebuilt import create_react_agent from l402_requests.integrations.langchain import L402FetchTool, L402SpendingTool # Create tools tools = [L402FetchTool(), L402SpendingTool()] # Create agent llm = ChatOpenAI(model="gpt-4o") agent = create_react_agent(llm, tools) # Run result = agent.invoke({ "messages": [("user", "Get the weather forecast for NYC from agent-commerce.store")] }) ``` The agent will call `l402_fetch`, automatically pay the Lightning invoice when it gets a 402, and return structured data. ## Tools ### L402FetchTool HTTP GET or POST with automatic L402 payment handling. ```python from l402_requests import L402Client, BudgetController from l402_requests.integrations.langchain import L402FetchTool # Custom budget client = L402Client( budget=BudgetController( max_sats_per_request=500, max_sats_per_hour=5000, ) ) tool = L402FetchTool(client=client) ``` **Parameters:** - `url` (str) — The full URL to request - `method` (str) — `GET` or `POST` (default: `GET`) - `body` (str, optional) — JSON string body for POST requests ### L402SpendingTool Check how many sats have been spent in this session. ```python from l402_requests.integrations.langchain import L402SpendingTool tool = L402SpendingTool(client=client) # Share the same client ``` ## Shared Client Pass the same `L402Client` to all tools so they share credential cache and budget: ```python from l402_requests import L402Client, BudgetController from l402_requests.integrations.langchain import L402FetchTool, L402SpendingTool client = L402Client( budget=BudgetController(max_sats_per_request=1000), ) tools = [ L402FetchTool(client=client), L402SpendingTool(client=client), ] ``` ## Wallet Options The wallet is auto-detected from environment variables: | Priority | Wallet | Environment Variable | |----------|--------|---------------------| | 1 | LND | `LND_REST_HOST` + `LND_MACAROON_HEX` | | 2 | NWC | `NWC_CONNECTION_STRING` | | 3 | Strike | `STRIKE_API_KEY` | | 4 | OpenNode | `OPENNODE_API_KEY` | **Recommended:** Strike — full L402 support, no infrastructure required. ============================================================================== # Troubleshooting Source: https://docs.lightningenable.com/troubleshooting ============================================================================== # Troubleshooting This guide covers the most common issues merchants encounter when integrating with Lightning Enable, along with step-by-step instructions to diagnose and resolve each one. --- ## 401 Unauthorized A `401` response means the API could not authenticate your request. There are several possible causes. ### Missing or Invalid API Key **Error response:** ```json { "error": "API key required", "message": "Please provide API key in X-API-Key header" } ``` **Troubleshooting steps:** 1. Verify you are sending the `X-API-Key` header with every request: ```bash curl -H "X-API-Key: YOUR_API_KEY" https://api.lightningenable.com/api/merchant/me ``` 2. Confirm the key has no extra whitespace or line breaks. Copy it fresh from the dashboard at `https://api.lightningenable.com/dashboard/settings` (the key is not included in any email). 3. There is one Lightning Enable environment, `https://api.lightningenable.com`, and no separate dev/prod key. 4. If the key was recently regenerated, the old key is immediately invalidated. Update all clients and services to use the new key. 5. A key that is wrong, or belongs to a deactivated account, returns `401` with `"error": "Invalid API key"`. If the key is right, email support@lightningenable.com to restore the account. **See also:** [Authentication](/api-reference/authentication) ### Expired or Inactive Subscription If your subscription is canceled, past due, or unpaid, the API key still authenticates but requests return `403` (see [403 Forbidden](#403-forbidden) below). --- ## 402 Payment Required A `402` response is **not an error** -- it is the first step of the L402 payment flow. The server is telling your client that it needs to pay a Lightning invoice before accessing the requested resource. **Example response:** ```http HTTP/1.1 402 Payment Required WWW-Authenticate: L402 macaroon="AgEB...", invoice="lnbc..." { "error": "Payment Required", "message": "Pay the Lightning invoice to access this API", "l402": { "macaroon": "AgEB...", "invoice": "lnbc100n1p...", "amount_sats": 10, "payment_hash": "abc123...", "expires_at": "2026-01-09T13:00:00Z" }, "instructions": { "step1": "Pay the Lightning invoice using any Lightning wallet", "step2": "Copy the preimage (proof of payment) from your wallet", "step3": "Include in request: Authorization: L402 :" } } ``` **What to do:** 1. Pay the Lightning invoice included in the `l402.invoice` field using any Lightning wallet. 2. Obtain the **preimage** (proof of payment) from the wallet after payment succeeds. 3. Retry the original request with the `Authorization` header: ```bash curl https://api.lightningenable.com/l402/proxy/{proxyId}/endpoint \ -H "Authorization: L402 :" ``` 4. The token can be reused for subsequent requests until it expires (default: 1 hour). **If you sent a token and still got a 402:** the token was refused, and the response is a fresh challenge. The reason is in the body `message` and the `X-L402-Error` header. See [Common L402 Errors](#common-l402-errors). **If you did not expect a 402:** - You may be hitting an L402-protected proxy endpoint (`/l402/proxy/*`) rather than a standard API endpoint. - Standard API endpoints (`/api/payments`, `/api/refunds`, etc.) never return 402. **See also:** [L402 API](/api-reference/l402), [How It Works](/products/agentic-commerce/how-it-works) --- ## 403 Forbidden A `403` response means your identity is confirmed but you are not authorized to perform the requested action. ### Account Inactive ```json { "error": "Account inactive", "message": "Your account is inactive. Please contact support.", "action_required": "contact_support" } ``` **Resolution:** Contact support@lightningenable.com to reactivate your account. ### Subscription Required ```json { "error": "Subscription required", "message": "Your plan tier requires an active subscription. Please subscribe to continue using the service.", "current_plan": "individual", "action_required": "subscribe" } ``` **Resolution:** Start the [Agentic Commerce card trial](https://api.lightningenable.com/Checkout?plan=individual), or [renew in sats](/getting-started/activate-with-lightning#renew-in-sats). ### Subscription Not Active (Past Due / Canceled) ```json { "error": "Subscription not active", "message": "Your subscription payment is past due. Please update your payment method to continue using the service.", "subscription_status": "past_due", "action_required": "update_payment_method" } ``` `action_required` is `update_payment_method` when the status is `past_due`, and `renew_subscription` for every other status (`canceled`, `unpaid`, `incomplete`, `incomplete_expired`). **Resolution:** Access the Stripe customer portal via `POST /api/stripe/customer-portal` (authenticated with your merchant API key; pass a `returnUrl` in the body). The response contains a `portalUrl` — open it to update your payment method or renew. ### Feature Not Available on Your Plan ```json { "error": "Feature not available", "message": "Refund processing is not enabled for your account. Please contact support.", "feature": "refunds", "current_plan": "free", "required_plan": null, "action_required": "contact_support" } ``` **Plan feature matrix:** | Feature | Free Producer Sandbox ($0) | Agentic Commerce ($49/mo) | Agentic Commerce — Business (contact us) | |---------|----------------------------|----------------------------------------|----------------------------------------| | Payments & Invoices | Yes | Yes | Yes | | Webhooks | 1 endpoint | 5 endpoints | 5 endpoints | | Multi-currency conversion | Yes | Yes | Yes | | L402 (server-side) | Capped | Yes | Yes | | Refunds | By request | By request | By request | **Refunds are not on any plan.** No tier turns them on — `refundsEnabled` is a per-account flag an operator sets on request. That is why the refunds `403` sends `required_plan: null` and `action_required: "contact_support"`: upgrading would not grant the feature. **Resolution:** Read `action_required`, not the plan name. - `contact_support` — email support@lightningenable.com. Upgrading will not help. - `upgrade_plan` — the plan named in `required_plan` grants the feature. If `required_plan` already matches your `current_plan`, the account flag was never applied to your row; contact support rather than buying the plan again. Key your handling on the `feature` field. `current_plan` is the normalized tier — one of `free`, `individual`, or `l402` — even when your account was created under an older tier id. There are two exceptions: an account carrying a tier value the API does not recognize is echoed back raw, and an account with a blank tier (no plan on file) is reported as `null`; handle an unexpected value rather than assuming the enum. **See also:** [Error Reference](/api-reference/errors#subscription-errors) --- ## 413 Payload Too Large The L402 proxy enforces size limits on both request and response bodies. **Error response:** ```json { "error": "Payload Too Large", "message": "Request body size (2,500,000 bytes) exceeds the maximum allowed size (1,048,576 bytes)", "proxy_id": "my-api-1234" } ``` **Default limits:** | Direction | Default Limit | |-----------|---------------| | Request body (client to proxy) | 1 MB (1,048,576 bytes) | | Response body (target API to proxy) | 10 MB (10,485,760 bytes) | **Troubleshooting steps:** 1. Check if your request payload exceeds 1 MB. Reduce the payload size or split it into smaller requests. 2. If the target API returns responses larger than 10 MB, the proxy will return a `502 Bad Gateway` instead (see [502 Bad Gateway](#502-bad-gateway) below). 3. These limits are server-wide defaults configured via `L402:MaxProxyRequestBodyBytes` and `L402:MaxProxyResponseBodyBytes`. Contact support if your use case requires higher limits. **Note:** The size check applies both when `Content-Length` is present and when it is absent (streaming). In the streaming case, the proxy reads the body incrementally and rejects it as soon as it exceeds the limit. --- ## 429 Too Many Requests There are **two distinct 429s**. Tell them apart by the `error` string in the body. ### Rate limit exceeded (too many requests) Rate limiting protects system stability. When you exceed your quota, you receive: ```json { "error": "Too many requests", "message": "Rate limit exceeded. Please try again later.", "retryAfter": 60 } ``` The wait time is in the `Retry-After` header and the `retryAfter` body field (seconds). The API does not send `X-RateLimit-*` headers. **Rate limits by policy:** | Policy | Limit | Window | Applied To | |--------|-------|--------|------------| | Global | 100 req | 1 min | All requests (per API key, or per IP when anonymous) | | Read | 200 req | 1 min | GET operations | | Payment Create | 10 req | 1 min | POST /api/payments, POST /api/refunds | | Write | 20 req | 1 min | Merchant self-service writes (e.g., key regeneration) | | Checkout Create | 5 req | 1 min | Stripe checkout session creation | | Admin | 30 req | 1 min | Internal admin endpoints | | Merchant key | 600 req | 1 min | Producer surface: L402 challenges, proxy management, manifest, merchant settings (per API key) | **Troubleshooting steps:** 1. **Read `Retry-After`** and wait that many seconds before retrying. 2. **Use webhooks instead of polling.** If you are polling payment status, switch to webhook notifications. Webhooks push events to your server in real time and consume zero API quota. 3. **Cache responses.** Exchange rates and payment status do not change every second. Cache GET responses for a reasonable TTL. 4. **Implement exponential backoff** with jitter for retries on 429 responses. ### Too many failed authentication attempts (auth-failure throttle) A different 429: more than 20 **failed** authentications (missing or invalid API key) from your IP within a 60-second window blocks further authenticated requests from that IP. This one **does** set a `Retry-After` header: ```http HTTP/1.1 429 Too Many Requests Retry-After: 42 { "error": "Too many failed authentication attempts", "message": "Slow down and try again in 42 seconds" } ``` If you see this, your integration is repeatedly sending a wrong or stale API key — **fix the key, don't retry**. Retrying with the same bad key records more failures and keeps the block engaged. Verify your key at **Dashboard → Settings**, update your configuration, then wait out the `Retry-After` seconds. **See also:** [Rate Limiting](/api-reference/rate-limiting) --- ## 502 Bad Gateway A `502` means the proxy or API could not get a valid response from an upstream service. ### L402 Proxy: Target API Unreachable ```json { "error": "Bad Gateway", "message": "Unable to connect to the target API", "proxy_id": "my-api-1234", "details": "Connection refused" } ``` **Troubleshooting steps:** 1. Verify the target API is online by making a direct request to the `targetBaseUrl`. 2. Check that the target URL is correct in your proxy configuration. 3. Test connectivity using the proxy test endpoint: ```bash curl -X POST https://api.lightningenable.com/api/proxy/{proxyId}/test \ -H "X-API-Key: your-api-key" ``` ### L402 Proxy: SSRF Protection Blocking Requests The L402 proxy includes SSRF (Server-Side Request Forgery) protection that blocks requests to internal network addresses. If your target URL resolves to a private IP, the proxy will reject it with a `502`. **Blocked addresses include:** - `localhost`, `127.0.0.1`, `::1` - Private IP ranges: `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16` - Link-local addresses: `169.254.0.0/16` - Loopback hostname variants: `localhost.localdomain`, `ip6-localhost` - Hostnames ending in `.localhost` **Resolution:** The target API must be hosted on a publicly routable IP address. If your API runs on a private network, expose it through a reverse proxy or API gateway with a public hostname first. **Note:** SSRF validation occurs both when you create/update a proxy (at configuration time) and again at request time (to protect against DNS rebinding attacks where a domain initially resolves to a public IP but later changes to an internal IP). ### L402 Proxy: Response Too Large ```json { "error": "Bad Gateway", "message": "Response from target API (15,000,000 bytes) exceeds the maximum allowed size (10,485,760 bytes)", "proxy_id": "my-api-1234" } ``` **Resolution:** The target API returned a response larger than 10 MB. Either configure the target API to return smaller responses (pagination, filtering) or contact support about raising the limit. ### Strike API Errors Errors from Strike surface with the upstream Strike status code in the error details — a `401`/`403` from Strike means your Strike API key was rejected; a `5xx` means Strike itself is having trouble. **Troubleshooting steps:** 1. Validate your Strike API key against Strike's API: ```bash curl -X POST https://api.lightningenable.com/api/merchant/validate-strike \ -H "X-API-Key: your-merchant-api-key" ``` If validation fails, re-save the key (`PUT /api/merchant/strike-key` or the dashboard) and confirm it has the required scopes (`partner.receive-request.read`, `partner.receive-request.create`, `partner.webhooks.manage`). 2. **Refund failures:** Strike has no dedicated refund API — Lightning Enable processes Strike refunds as new **outgoing payments** to the customer's Lightning invoice. A refund can therefore fail if your Strike account balance is insufficient to send the payment. 3. **Delayed status updates:** Strike webhooks are "thin" (they carry only an entity ID), so Lightning Enable fetches the full payment details from Strike's API after each webhook. A transient Strike API error during that fetch can briefly delay a status update; use `POST /api/payments/{invoiceId}/sync` to force a re-check. 4. Check if Strike is experiencing an outage. **See also:** [Strike Setup](/strike-setup/account-setup) ### OpenNode API Errors ```json { "error": "OpenNode API error", "details": "Invalid API key" } ``` **Troubleshooting steps:** 1. Verify your OpenNode API key is correctly configured via the merchant settings endpoint, and validate it with `POST /api/merchant/validate-opennode`. It must be a **production** key — the hosted platform does not use OpenNode's dev environment. 2. Ensure your OpenNode account is active and verified. 3. Check if OpenNode is experiencing an outage. 4. If you see repeated 5xx errors from OpenNode, the circuit breaker may open (returning `503`). It auto-recovers after 30 seconds. **See also:** [OpenNode Setup](/opennode-setup/account-setup) --- ## Webhook Delivery Failures Webhooks are how Lightning Enable notifies your server about payment events. If they are not arriving, use this checklist to diagnose the problem. ### Webhooks Not Received **Step 1: Check your webhook URL configuration.** ```bash curl https://api.lightningenable.com/api/merchant/me \ -H "X-API-Key: your-api-key" ``` `onboarding.hasWebhookUrl` must be `true`. The URL itself isn't echoed back; to change it, call `PUT /api/merchant/webhook-url`. Use HTTPS. Common delivery failures, by the status code your endpoint returns: | Status Code | Meaning | Fix | |-------------|---------|-----| | 0 / timeout | Your server did not respond within 10 seconds (every delivery attempt, including retries, times out at 10 s) | Return 200 immediately, process asynchronously | | 403 | Firewall blocking inbound requests | Allow inbound HTTPS from the internet to your webhook path and verify `X-LightningEnable-Signature` for authenticity (Lightning Enable's outbound IPs are not static, so don't rely on an IP allowlist) | | 404 | Wrong webhook path | Correct the URL in merchant settings | | 500 | Your handler threw an error | Fix the error in your webhook handler | *(As of November 2026, every delivery attempt, including each retry, times out at 10 seconds. Previously the retries waited up to 100 seconds, so a receiver that took longer than 10 seconds could still succeed on a retry. If your handler takes longer than that, acknowledge with a 2xx first and process the event asynchronously.)* **Step 2: Test your endpoint manually.** ```bash curl -X POST https://your-site.com/webhooks/lightning \ -H "Content-Type: application/json" \ -d '{"invoiceId": "test", "orderId": "ORDER-TEST", "status": "paid", "amount": 1.00, "currency": "USD"}' ``` If this returns a non-200 response, the problem is on your server. **Step 3: Recover the missed event.** Failed deliveries are retried with exponential backoff (30s/60s/120s/240s/480s, 5 retry attempts, ~16-minute window). If your endpoint was down longer than that, reconcile with the authoritative status: ```bash # Authoritative payment status curl https://api.lightningenable.com/api/payments/{invoiceId} \ -H "X-API-Key: YOUR_API_KEY" # Or force a re-check against your payment provider curl -X POST https://api.lightningenable.com/api/payments/{invoiceId}/sync \ -H "X-API-Key: YOUR_API_KEY" ``` ### Webhook Signature Verification Failing Lightning Enable signs outbound webhooks with the `X-LightningEnable-Signature` header using the format: ``` X-LightningEnable-Signature: t={timestamp},v1={hmac_sha256_signature} ``` **Common causes of signature verification failure:** 1. **Wrong secret.** Make sure you are using the correct webhook secret. For payment webhooks, this is the secret you configured for your merchant account. For Stripe subscription events forwarded by Lightning Enable, this is the `SubscriptionForwardSecret`. 2. **Parsed body vs. raw body.** The HMAC must be computed over the **raw request body bytes**, not over a re-serialized JSON object. Parsing and re-serializing JSON can change key ordering, whitespace, or unicode escaping, producing a different signature. 3. **Incorrect algorithm.** Use HMAC-SHA256. 4. **Timestamp drift.** The `t=` prefix contains the timestamp when the signature was generated. If you are validating timestamp freshness, allow a reasonable tolerance (e.g., 5 minutes). **Verification example (Node.js):** ```javascript const crypto = require('crypto'); function verifyWebhook(rawBody, signatureHeader, secret) { // Parse "t=123456,v1=abcdef..." const parts = Object.fromEntries( signatureHeader.split(',').map(p => p.split('=')) ); const timestamp = parts.t; const signature = parts.v1; const payload = `${timestamp}.${rawBody}`; const expected = crypto .createHmac('sha256', secret) .update(payload) .digest('hex'); return crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(expected) ); } ``` **See also:** [Webhooks](/api-reference/webhooks) --- ## Common L402 Errors When an L402-protected proxy refuses a token, it answers with a fresh `402` challenge. The reason is in the body `message` and the `X-L402-Error` header. In every case, the fix ends the same way: pay the new challenge and retry with the new token. ### Invalid Preimage Format **Message:** `Invalid preimage format (must be 64 hex characters)` **Cause:** The preimage in your `Authorization: L402 :` header is not the correct format. **Requirements:** - Exactly 64 hexadecimal characters (representing 32 bytes) - Lowercase hex encoding (e.g., `a1b2c3d4...`) - No `0x` prefix **Troubleshooting steps:** 1. Check your wallet output. Some wallets return the preimage in base64 instead of hex. Convert it: ```bash echo -n "" | base64 -d | xxd -p -c 64 ``` 2. Verify the preimage length. If it is shorter or longer than 64 characters, it is the wrong value. Some wallets return the payment hash (which is the SHA-256 of the preimage), not the preimage itself. 3. Ensure you are not accidentally including whitespace or newlines. ### Preimage Does Not Match Payment Hash **Message:** `Preimage does not match payment hash` **Cause:** `SHA256(your_preimage) != payment_hash_in_macaroon`. This means either: - You are using the preimage from a **different** payment. - Your wallet returned the wrong value (e.g., payment hash instead of preimage). - The invoice expired and you paid a different invoice but are using the old macaroon. **Resolution:** Pay the specific invoice from the 402 response and use the macaroon and preimage from the same challenge. The macaroon and preimage are a matched pair -- you cannot mix them across different 402 challenges. ### Cross-Tenant Token Reuse **Message:** `Token not valid for this merchant` **Cause:** Each L402 token is bound to a specific merchant via a `merchant_id` caveat. You cannot use a token obtained from one merchant's proxy to access a different merchant's proxy. **Related errors:** | Error Message | Meaning | |---------------|---------| | `Token requires merchant context but none was provided` | The token has a merchant_id caveat but the request context does not | | `Token missing required merchant_id caveat` | The request context has a merchant ID but the token does not contain a merchant_id caveat | | `Token not valid for this merchant` | The token's merchant_id does not match the proxy's merchant | **Resolution:** Obtain a new token by requesting the correct proxy endpoint without an `Authorization` header to receive a fresh 402 challenge. ### Token Not Valid for Path **Message:** `Token not valid for path '/l402/proxy/api-b/data'. Token is bound to '/l402/proxy/api-a/*'` **Cause:** L402 tokens contain a path caveat restricting which endpoints they can access. A token issued for one proxy cannot be used on a different proxy. **Resolution:** Request a new token from the correct proxy endpoint. ### Amount Mismatch (Cross-Endpoint Replay) **Message:** `Token amount does not match required price` **Cause:** A token paid at one price tier (e.g., 10 sats for a demo endpoint) cannot be used on an endpoint with a different price (e.g., 50 sats for a premium endpoint), even if both are under the same proxy. **Resolution:** Request a new token for the specific endpoint you want to access. ### L402 Verification Failed **Message:** `L402 verification failed`, or a caveat failure such as an expired token **Cause:** The token has expired, or the macaroon signature is invalid because it was altered or corrupted. **Resolution:** Request a fresh 402 challenge and pay the new invoice. --- ## Still Stuck? If this guide did not resolve your issue: 1. Check the [Error Reference](/api-reference/errors) for the exact error message you are seeing. 2. Review the [FAQ](/faq) for common questions. 3. Email **support@lightningenable.com** with: - The exact error response (HTTP status code + JSON body) - The request you are making (endpoint, headers, method) - Your merchant ID or API key prefix (first 10 characters only -- never share the full key) - Any relevant correlation IDs from 500 error responses