Skip to content

Webhook notifications

The sidecar can send webhook notifications when payments are finalized (completed or failed). This is useful for tracking payment status without polling.

Set LEXE_WEBHOOK_URL in your environment or pass --webhook-url to the sidecar binary:

# Via environment variable
$ export LEXE_WEBHOOK_URL="https://example.com/webhooks/lexe"

# Via CLI argument
$ lexe-sidecar --webhook-url https://example.com/webhooks/lexe

When a payment is finalized, the sidecar will POST a JSON payload to your webhook URL:

{
  // Fields from the "Standard Webhooks" spec: https://www.standardwebhooks.com
  "type": "payment.finalized",
  "timestamp": "2025-04-17T21:54:17.989Z",

  // Originating wallet, useful for multi-wallet setups
  "user_pk": "63ad1661bfc23ad25f5bcc6f610f8fd70d7426de51be74766c24e47f4b4fcfca",

  // Payment fields
  "index": "0000001744926519917-ln_9be5e4e3a0356cc4a7a1dce5a4af39e2896b7eb7b007ec6ca8c2f8434f21a63a",
  "rail": "invoice",
  "kind": "invoice",
  "direction": "inbound",
  "hash": "9be5e4e3a0356cc4a7a1dce5a4af39e2896b7eb7b007ec6ca8c2f8434f21a63a",
  "preimage": "a1b2c3d4e5f6789012345678901234567890123456789012345678901234abcd",
  "offer_id": null,
  "txid": null,
  "amount": "1000",
  "fees": "0",
  "partner_pk": null,
  "partner_prop_fee": null,
  "partner_base_fee": null,
  "status": "completed",
  "status_msg": "completed",
  "address": null,
  "invoice": "lnbc10n1p5qz7z2dq...",
  "tx": null,
  "payer_name": null,
  "message": null,
  "personal_note": null,
  "priority": null,
  "expires_at": 1744930119917,
  "finalized_at": 1744926857989,
  "created_at": 1744926519917,
  "updated_at": 1744926857989
}

The payment fields use the same response schema as GET /v2/node/payment, flattened alongside a user_pk field that identifies the wallet (useful for multi-wallet setups) and top-level type and timestamp fields from the Standard Webhooks spec.

Your endpoint should return a 200 status code to indicate success.

NOTE: All webhook handling must be idempotent. In rare occasions, you might receive a payment notification more than once.

(Optional) Authenticating webhook events

Webhook authentication is needed if your webhook receiver is publicly reachable; for example, LEXE_WEBHOOK_URL=https://myapp.com/api/webhooks. If you run the webhook receiver on the same machine as the sidecar (e.g. LEXE_WEBHOOK_URL=http://localhost:8000), you can skip this section. Otherwise, anyone who can reach your webhook receiver could POST a fake webhook and trick your receiver into thinking a payment was completed when it wasn't.

To prevent this, you should configure the sidecar with a secret shared between the sidecar and the webhook receiver, and verify the signature on every request.

Lexe's sidecar signs outbound webhooks using the Standard Webhooks HMAC-SHA256 scheme. The shared secret is a random 24–64 byte string, base64-encoded and conventionally prefixed with whsec_. To generate one, use any cryptographically secure RNG, e.g.:

# Using openssl
$ openssl rand -base64 24 | sed 's/^/whsec_/'
whsec_yb5FARH2Kl2CCKkVB9bHXPuLpEy4v9qR

# Using python3
$ python3 -c 'import secrets, base64; print("whsec_" + base64.b64encode(secrets.token_bytes(24)).decode())'
whsec_Ynf2Ro771KA7e/kDOxu+4iWy/7OJExE4

Then configure the sidecar with the secret via LEXE_WEBHOOK_SECRET or --webhook-secret:

# Via environment variable
$ export LEXE_WEBHOOK_SECRET="whsec_C2FVsBQIhrscChlQIMV+b5sSYspob7oD"

# Via CLI argument
$ lexe-sidecar --webhook-secret whsec_C2FVsBQIhrscChlQIMV+b5sSYspob7oD

If a secret is configured, the sidecar adds three additional headers to every webhook request it sends:

  • webhook-id: An opaque identifier for this delivery, stable across retries. Safe to use as an idempotency key.
  • webhook-timestamp: The unix timestamp (seconds) of the delivery attempt.
  • webhook-signature: The signature for this delivery (see below).

Each signature is an HMAC-SHA256 over the raw HTTP body, event id, and delivery timestamp, tagged with a v1 prefix, and signed using the shared secret. Specifically, the webhook-signature header value is "v1," + base64(HMAC-SHA256(<secret>, "<id>.<timestamp>.<body>")). Full details are in the Standard Webhooks spec.

To verify signatures, the easiest way is to use one of the official Standard Webhooks libraries — implementations exist for C#, Elixir, Go, Java, JavaScript, PHP, Python, Ruby, and Rust. Alternatively, verify the signatures yourself; the full spec is short and easy to follow.

Delivery guarantees and persistence

For each finalized payment, the sidecar attempts delivery up to three times with exponential backoff. To ensure that delivery attempts are made for every payment created by the sidecar, even across sidecar restarts, the sidecar persists the set of watched payments to ~/.lexe/ (override with LEXE_DATA_DIR or --data-dir).

NOTE: Any per-request client credentials submitted via the Authorization header are also persisted alongside the tracking state so the sidecar can resume polling them on restart.