Register a webhook
For Lightning receives, register on the wallet that creates the invoice. That wallet receives notifications even when the invoice names another receiver. Receiver-side delivery is not a substitute for registering on the requesting wallet.event_types and webhook_id, even though most wallet methods use camelCase. Use a random ASCII secret of at least 16 characters. The example generates 32 random bytes and registers them as a 64-character hex string.
The endpoint must use HTTPS, and its hostname must resolve when registering. A newly created tunnel can fail with Failed to resolve hostname; wait for DNS, then retry registration with bounded backoff. Do not mark notifications as enabled until registration succeeds.
Verify the raw request body
Each webhook includesX-Spark-Signature: a hex-encoded HMAC-SHA256 of the raw HTTP body, keyed with the registered secret. Use SHA-256; implementations for other products with a similarly named header are not interchangeable.
JSON.stringify(parsedBody): whitespace and serialization changes alter the signature. The generated secret is used as the literal string provided when registering the webhook, not decoded from hex into bytes.
Reject missing or invalid signatures. After verification, validate the payload and durably record or queue the event before acknowledging it. Signature verification does not prevent replay; processing must also be idempotent.
Match a Lightning receive
Persist the full receive-request ID andinvoice.paymentHash when creating an invoice. The webhook’s receive-request id is a bare UUID, while the SDK returns SparkLightningReceiveRequest:<UUID>. A direct string comparison misses the match.
Normalize the request ID for the database lookup while retaining the original SDK ID for API calls:
id, status, request_status, payment_preimage, receiver_identity_public_key, and amounts. The receive payload does not include the Spark transfer ID. Look up the stored SDK request ID as the requester or receiver and read request.transfer?.sparkId. If the transfer is not present yet, retry the lookup later. See invoice-to-transfer correlation.
A
SPARK_LIGHTNING_RECEIVE_FINISHED notification reports a terminal outcome; the name alone is not proof of success. Check request_status and the receive-flow status before crediting a payment. Reconcile pending requests through lookups as well: do not assume every failure or cancellation will produce a notification. Request completion is separate from the receiver claiming the funds and making them spendable.
Amounts are objects, not bare numbers. For example, { "value": 1000000, "unit": "MILLISATOSHI" } represents 1,000 sats. Do not assume htlc_amount always uses the same unit or is always present. SDK amount objects use originalValue and originalUnit instead.
Delivery and retries
Webhook delivery is retried for HTTP408, 429, and 5xx responses, as well as unexpected transport failures. Other client errors, including 401, are not retried. A registration-time DNS error means no webhook was registered; it is separate from a delivery failure after registration.
Use a retryable response when you cannot durably accept an event. A short observation window without another delivery is not evidence that retries are disabled. Do not depend on a fixed retry interval, count, or ordering between webhooks, operator events, and payment-request lookups.
Handle repeated notifications without applying the same payment twice. Keep pending requests in your database and reconcile them through payment-request lookups after downtime; a missing webhook is not proof that a payment failed.