Skip to main content

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 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.

EndpointWhat it does
POST /api/l402/challengesMint an invoice + macaroon for a resource
GET /api/l402/challengesList what you have minted, with payment status
GET /api/l402/challenges/{paymentHash}Look up one challenge
POST /api/l402/challenges/verifyVerify a macaroon + preimage
POST /api/l402/challenges/verify-credentialVerify a modern Payment bearer token (single-use)

Base URL​

https://api.lightningenable.com

Authentication​

Every request requires your Lightning Enable merchant API key in the X-API-Key header. Generate one at Dashboard → Settings → API Keys.

X-API-Key: <your-merchant-api-key>

The key is tied to your merchant account and to an Agentic Commerce subscription (Agentic Commerce at $49/mo, or Business — contact us). L402 must be enabled on your plan — Native mode is included with both Agentic Commerce tiers.

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​

POST /api/l402/challenges HTTP/1.1
Host: api.lightningenable.com
X-API-Key: <your-merchant-api-key>
Content-Type: application/json
Idempotency-Key: <optional, retry-safe key>

{
"resource": "/api/premium/weather",
"priceSats": 100,
"description": "Premium weather forecast"
}

Body:

FieldTypeRequiredNotes
resourcestring (≤ 848 chars)yesThe 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.
priceSatsinteger (≥ 1)yesPrice in satoshis.
descriptionstring (≤ 500 chars)noEmbedded 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.
idempotencyKeystring (≤ 200 chars)noSame meaning as the Idempotency-Key header, for clients that can't set headers. The header wins if you send both. See Idempotency.

Headers:

HeaderRequiredNotes
X-API-KeyyesMerchant API key
Content-Typeyesapplication/json
Idempotency-KeynoIf 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.
X-Idempotency-KeynoThe spelling this API shipped with. Still accepted and identical in behaviour; Idempotency-Key wins if you send both.

Response — 200 OK​

{
"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\""
}
FieldTypeNotes
invoicestringBOLT11 Lightning invoice the caller must pay
macaroonstringURL-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.
paymentHashstringHex payment hash linking the macaroon to the invoice
expiresAtstring (ISO 8601)When the Lightning invoice expires
resourcestringEchoes the request's resource
priceSatsintegerEchoes the request's priceSats
mppChallengestring | nullReady-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.

Error responses​

Errors on this endpoint are RFC 9457 problem documents — application/problem+json, with a stable type URI to branch on and the pre-RFC error / message members still present.

Statustype suffixMeaning
400—Data-annotation failure (missing resource, resource over 848 characters, priceSats < 1). Returns ASP.NET Core's model-state ProblemDetails ({ "type", "title", "errors": { ... } }), which has no Lightning Enable type suffix.
400invalid_idempotency_keyThe key was blank or longer than 200 characters.
400invalid_resourceresource exceeds the 848-character limit. Carries max_length.
400payment_provider_not_configuredNo Strike or OpenNode key on your account, so no invoice could be created. Carries docs. This is the most common refusal on a brand-new account.
400challenge_creation_failedAnother business-logic refusal from the challenge service.
401authentication_required / merchant_not_foundMissing, malformed, or revoked X-API-Key.
402plan_price_limit / plan_volume_limit / plan_endpoint_limitA plan cap was reached. Each carries current_plan and upgrade_url.
403—L402 is not enabled on your plan. Body includes current_plan and action_required: "upgrade_plan".
409idempotency_key_reuseThe key was already used for a different resource or priceSats. Carries bound_resource / bound_price_sats and requested_resource / requested_price_sats.
409endpoint_retiredThe resource was retired to free a plan slot and can no longer mint.
429—Rate-limited.
503challenge_persist_failedThe 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​

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: <your-merchant-api-key>
Query parameterTypeDefaultNotes
statuspaid | unpaid | expirednoneOmit to list everything. Any other value is a 400.
sinceISO 8601 timestampnoneLower bound on createdAt. Send an offset (Z or +02:00); a bare timestamp is read as UTC.
limitinteger50Clamped to 1..200 rather than rejected.
offsetinteger0Clamped to at least 0.

Response — 200 OK​

The total matching your filter, ignoring paging, is also in the X-Total-Count header.

{
"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
}
FieldNotes
paymentHashHex payment hash of the challenge's invoice. The correlation handle — safe to log and store.
statuspaid 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.
paidAtWhen 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.
expiresAtEnd of the token window; matches the macaroon's expires caveat. null on rows minted before this was recorded.
idempotencyKeyThe key the challenge was minted under, if you sent one.
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​

Statustype suffixMeaning
400invalid_status_filterstatus was not one of the three values. Carries allowed_status.
401authentication_requiredMissing or invalid X-API-Key.

GET /api/l402/challenges/{paymentHash}​

Look up a single challenge you minted.

GET /api/l402/challenges/abc123... HTTP/1.1
X-API-Key: <your-merchant-api-key>

Returns the same object as one element of the list above.

Error responses​

Statustype suffixMeaning
400invalid_payment_hashNot 64 hexadecimal characters.
401authentication_requiredMissing or invalid X-API-Key.
404challenge_not_foundYou 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​

POST /api/l402/challenges/verify HTTP/1.1
Host: api.lightningenable.com
X-API-Key: <your-merchant-api-key>
Content-Type: application/json

{
"macaroon": "AgELbWFjYXJvb24=...",
"preimage": "deadbeef..."
}

Body:

FieldTypeRequiredNotes
macaroonstringrequired for L402The 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).
preimagestring (hex, 64 chars)yesThe payment preimage proving the invoice was paid.
resourcestring | nullrecommendedThe 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).
amountSatsinteger | nullrecommendedThe 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.
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:

{
"valid": true,
"resource": "/api/premium/weather",
"merchantId": 42,
"amountSats": 100,
"paymentHash": "abc123..."
}

Invalid token:

{
"valid": false,
"error": "Invalid preimage"
}
FieldTypeNotes
validboolThe gate. Inspect this.
errorstring | nullFailure reason; only populated when valid: false. Examples: "Invalid preimage", "Token bound to a different resource", "Macaroon signature invalid".
resourcestring | nullThe path/resource the token is bound to (from the macaroon's caveat). Assert this matches the resource the caller is actually requesting.
merchantIdinteger | nullThe merchant ID the macaroon was issued under.
amountSatsinteger | nullThe amount the token was issued for.
paymentHashstring | nullThe payment hash from the macaroon's identifier.

Error responses​

Also RFC 9457 problem documents.

Statustype suffixMeaning
400invalid_verification_requestA field was present but unusable — a blank macaroon, a blank resource, amountSats below 1.
400mpp_not_supportedPreimage-only verification requested but MPP is not enabled.
401authentication_required / merchant_not_foundMissing 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. 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 <base64url(JSON)> 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.

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​

POST /api/l402/challenges/verify-credential
X-API-Key: YOUR_MERCHANT_API_KEY
Content-Type: application/json
FieldTypeRequiredNotes
credentialstringYesThe token as received. The leading Payment scheme word is optional.
resourcestringNoWhen set, the challenge must have been minted for this resource.
amountSatsintegerNoWhen set, the challenge must have been minted for exactly this price.

Response — 200 OK​

{
"valid": true,
"consumed": true,
"resource": "/api/premium/weather",
"merchantId": 42,
"amountSats": 100,
"paymentHash": "abc123...",
"receipt": "eyJjaGFsbGVuZ2VJZCI6Ims5UTMuLi4iLC..."
}
FieldNotes
validtrue when the preimage matches, the challenge binding is intact, the credential is unexpired, and it had not been consumed before.
consumedtrue when this call consumed the credential.
receiptbase64url(JCS) receipt: {"challengeId","method":"lightning","reference":"<payment hash>","status":"success","timestamp"}. Return it to the payer as the Payment-Receipt response header. The reference is the payment hash, never the preimage.
errorPresent when valid is false. A static description, never the token or preimage.

Error responses​

Statustype suffixMeaning
400mpp_not_supportedModern credentials are switched off on this server.
401authentication_required / merchant_not_foundMissing or invalid X-API-Key.
403l402_not_enabledL402 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.


Error format​

Every error the producer API returns itself is application/problem+json (RFC 9457):

{
"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.

One exception: request-shape failures caught by model binding (a missing resource, priceSats below 1) come from ASP.NET Core's own validation and use its { "type", "title", "errors": { … } } ProblemDetails, with no Lightning Enable type suffix. Read errors for the per-field detail there.


Token reuse within the validity window​

L402 tokens remain valid for repeated verifications until the macaroon's expires caveat passes. The default is 60 minutes from issuance (L402Options.DefaultTokenValiditySeconds = 3600). During that window the producer API returns valid: true for any verification of a valid macaroon + preimage pair, including replays of the same pair. This is intentional, not a bug.

Two separate durations to understand:

  • Token validity (60 min default) — controlled by DefaultTokenValiditySeconds, embedded as an expires caveat in the macaroon. This is the window during which a paid token can be re-presented and verified successfully.
  • Invoice expiry (10 min default) — controlled by InvoiceExpirySeconds. This is the window during which the Lightning invoice itself can be paid. After this, the invoice is dead and the macaroon is moot regardless of its expiry caveat.

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.

Pre-2026-05 (before the verify endpoint switched to context-aware verification), path and amount caveats were always read out but never compared; integrators had to do the comparison themselves. They still can, but passing resource / amountSats on the request now opts into stricter server-side checks. New integrations should pass them; existing integrations that already do client-side comparison can omit them without breaking anything.

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, and the burden of caching preimages on the consumer side is real (the open-source l402-requests clients don't do it by default).

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:

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 (L402Options.InvoiceExpirySeconds).
  • 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, nothing changes from before: the server deduplicates by (merchantId, clientIP, resource, priceSats) for the invoice window, using a per-process cache. That is usually right for middleware on a single server, and wrong behind a load balancer or across a restart — pass an explicit key in those cases.

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 above. (The first one also marks the challenge paid; see 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 one under Dashboard → Settings → Webhooks; with no callback configured, nothing is sent and nothing is queued.

{
"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 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.


End-to-end example flows​

Mint + present a challenge​

# 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:

$ 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​

# 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:

$ 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:

  -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 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)​

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 SDK or l402-express middleware.

.NET (without the SDK)​

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<Challenge>();

Prefer L402Server or L402Server.AspNetCore.

Python (no SDK yet — Phase 2 of the Native L402 roadmap)​

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()

A Python SDK (lightningenable-l402-server) and FastAPI middleware (lightningenable-fastapi-l402) are in development.

Go (no SDK yet — Phase 2 of the Native L402 roadmap)​

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)

A Go SDK (github.com/refined-element/l402-server-go) with net/http middleware is on the Phase 2 roadmap.


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 — an MCP agent driving every endpoint on this page end to end, from an empty account to a live, verified paid endpoint