> ## Documentation Index
> Fetch the complete documentation index at: https://docs.spark.money/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Verify payment notifications and match them to wallet requests.

Webhooks notify your application when a request finishes. Use them to wake a backend or update a payment record without keeping a wallet event listener running. A webhook does not claim incoming funds or grant access to private wallet history.

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

```typescript theme={null}
import { randomBytes } from "node:crypto";
import { SparkWalletWebhookEventType } from "@buildonspark/spark-sdk/types";

// Generate once, store securely, and use the same string in your verifier.
const secret = randomBytes(32).toString("hex");
const registration = await wallet.registerSparkWalletWebhook({
  secret,
  url: "https://example.com/webhooks/spark",
  event_types: [SparkWalletWebhookEventType.SPARK_LIGHTNING_RECEIVE_FINISHED],
});
if (!registration) {
  throw new Error("Webhook registration was not confirmed");
}
console.log("Webhook ID:", registration.webhook_id);
```

The SDK's webhook methods use `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.

```typescript theme={null}
const result = await wallet.listSparkWalletWebhooks();
for (const webhook of result?.webhooks ?? []) {
  console.log(webhook.webhook_id, webhook.url, webhook.event_types);
}

// To remove a stored registration:
await wallet.deleteSparkWalletWebhook({ webhook_id: webhookId });
```

Up to five webhook URLs can be registered per wallet. Registering the same URL updates its secret and event list; it does not create another registration or append events. Adding a sixth URL evicts the oldest registration, so list existing registrations before adding another.

## Verify the raw request body

Each webhook includes `X-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.

```typescript theme={null}
import { createHmac, timingSafeEqual } from "node:crypto";

export function verifySparkWebhook(
  rawBody: Buffer,
  signature: string | undefined,
  secret: string,
): boolean {
  if (!signature || !/^[0-9a-fA-F]{64}$/.test(signature)) return false;
  const expected = createHmac("sha256", secret).update(rawBody).digest();
  const received = Buffer.from(signature, "hex");
  return received.length === expected.length && timingSafeEqual(received, expected);
}
```

Capture the bytes before JSON parsing and pass the header value to this function. Do not verify `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 and `invoice.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:

```typescript theme={null}
function receiveRequestKey(id: string): string {
  const prefix = "SparkLightningReceiveRequest:";
  return id.startsWith(prefix) ? id.slice(prefix.length) : id;
}

// Store receiveRequestKey(invoice.id) alongside the original invoice.id.
// Apply the same normalization to the verified webhook's receive-request id.
```

This normalization is specific to Lightning receive requests. Do not strip prefixes from every request identifier, and do not mistake a delivery ID or webhook registration ID for a receive-request ID.

The receive notification includes request details such as `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](/wallets/deposit-from-lightning#match-the-invoice-request-and-transfer).

| Receive field | Meaning |
| - | - |
| `status` | Receive-flow status, such as `TRANSFER_COMPLETED` |
| `request_status` | Overall request outcome, such as `SUCCEEDED` |
| `htlc_amount` | Received amount as `{ value, unit }`, or `null` before an amount is recorded; read the unit from the payload |
| `invoice_amount` | Requested amount as `{ value, unit }`; normally `SATOSHI` |

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 HTTP `408`, `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.

## Event types

| Event | Request |
| - | - |
| `SPARK_LIGHTNING_RECEIVE_FINISHED` | Lightning receive |
| `SPARK_LIGHTNING_SEND_FINISHED` | Lightning send |
| `SPARK_COOP_EXIT_FINISHED` | Cooperative exit |
| `SPARK_STATIC_DEPOSIT_FINISHED` | Static deposit |
