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

# Webhooks

> Push notifications for policy / bond / marketplace events. HMAC-signed.

<Note>
  See [/concepts/lifecycle](/concepts/lifecycle) for the end-to-end flow (policy → trigger → bond → wait/sell decision).
</Note>

Polling is wasteful. Subscribe a URL, the API will POST JSON when something
happens.

## Subscribe

```ts theme={null}
const sub = await lumina.webhooks.create({
  url:    'https://my-bot.example.com/webhooks/lumina',
  events: ['policy_purchased', 'policy_triggered', 'bond_minted'],
})
console.log('STORE THIS NOW:', sub.secret)
```

The `secret` is a 32-byte hex string. **Returned exactly once.** Store it in a
secret manager — there's no API to retrieve it later.

## Available events

| Event               | Fires when                                                           |
| ------------------- | -------------------------------------------------------------------- |
| `policy_purchased`  | A `purchasePolicyFor` call completes successfully                    |
| `policy_triggered`  | A trigger is accepted on-chain and a bond is minted                  |
| `bond_minted`       | Same trigger event, surfaced from the BondVault perspective          |
| `bond_redeemed`     | A bond is **redeemed for \$LUMINA** at maturity (730 days post-mint) |
| `listing_created`   | Your wallet listed a bond on the marketplace                         |
| `listing_purchased` | Your listing was bought by another wallet                            |

Use `events: '*'` to subscribe to everything for the wallet (recommended for
new integrations).

## Payload schema

Every delivery is JSON with the same envelope:

```jsonc theme={null}
{
  "id":        "evt_01HXY…",        // unique event ID (use for de-dup)
  "event":     "policy_triggered",
  "createdAt": 1715974321,           // unix seconds
  "wallet":    "0xYourWalletAddress",
  "data":      { /* event-specific payload, see below */ }
}
```

Event-specific `data` shapes:

```jsonc theme={null}
// policy_purchased
{ "policyId": "0x…", "productId": "0x…", "productName": "FLASHBTC24-001",
  "coverageAmount": "1000000000", "premiumPaid": "12345678", "txHash": "0x…" }

// policy_triggered  /  bond_minted
{ "policyId": "0x…", "bondId": "202805", "faceValueUsdc": "800000000",
  "triggeredAt": 1715974321, "txHash": "0x…" }

// bond_redeemed
{ "bondId": "202805", "amountRedeemed": "800000000", "luminaPaid": "…", "txHash": "0x…" }

// listing_created  /  listing_purchased
{ "listingId": "…", "bondId": "202805", "amount": "50",
  "totalPriceUsdc": "49000000", "txHash": "0x…" }
```

## Headers on every delivery

| Header               | Value                                   |
| -------------------- | --------------------------------------- |
| `Content-Type`       | `application/json`                      |
| `X-Lumina-Signature` | `hex(HMAC-SHA256(rawBody, secret))`     |
| `X-Lumina-Event`     | The event name, e.g. `policy_purchased` |
| `X-Lumina-Delivery`  | A unique delivery ID for de-duplication |

## Verifying the signature (Express example)

```ts theme={null}
import crypto from 'crypto'
import express from 'express'

const app = express()

// IMPORTANT: use raw body, not the JSON-parsed one — the HMAC is over the
// exact bytes the API sent.
app.post('/webhooks/lumina', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.header('X-Lumina-Signature') ?? ''
  const expected = crypto.createHmac('sha256', process.env.LUMINA_WEBHOOK_SECRET!)
    .update(req.body)
    .digest('hex')

  if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
    return res.status(401).send('bad signature')
  }

  const payload = JSON.parse(req.body.toString('utf8'))
  console.log('event:', req.header('X-Lumina-Event'), payload)
  res.sendStatus(200)
})
```

Always compare with `crypto.timingSafeEqual` (or your language's equivalent
constant-time comparator) to prevent timing attacks. Always verify against
the **raw body**, never the JSON-decoded representation.

## Retry semantics

* **Acceptance**: any 2xx response counts as delivered.
* **4xx**: marked failed permanently (the receiver said no).
* **5xx / network**: retried with exponential backoff (30s · 2^attempt).
* **Max attempts**: 3 (so the longest gap from event to final failure is \~150s).

The worker polls the queue every 30 seconds. Latency from event to first
delivery attempt is therefore 0–30s.

Use `X-Lumina-Delivery` as a de-duplication key — the same event can be
re-delivered if your endpoint times out then later succeeds.

## List and revoke

```ts theme={null}
const subs = await lumina.webhooks.list()    // metadata only — no secret
await lumina.webhooks.delete(subs[0].id)     // owner-only
```

## Limits

* **One URL per wallet.** Re-registering the same `(wallet, url)` pair returns 409. Delete and re-create to rotate the secret.
* **Multiple URLs allowed.** Different URLs (e.g. dev/staging/prod) can coexist on the same wallet.
* **HTTPS required** in production. `http://localhost` is allowed for testing.
