# Funding webhooks

Instead of polling `get`, subscribe to webhooks to be notified the moment a funding session changes status. Funding events flow through Openfort's standard [webhook system](/docs/configuration/webhooks) and are signed the same way.

## Events

| Event | Fired when |
| --- | --- |
| `funding.session.updated` | A session transitions to `waiting_payment`, `processing`, `succeeded`, `bounced`, or `expired`. |

The payload follows the standard Openfort webhook envelope — a `type`, a `date`, and a `data` object (here, the full funding session):

```json
{
  "type": "funding.session.updated",
  "date": 1781250000,
  "data": {
    "id": "fnd_…",
    "object": "fundingSession",
    "status": "succeeded",
    "target": { "chain": "eip155:8453", "currency": "0x8335…2913", "address": "0xUser…" },
    "paymentMethod": {
      "type": "evm",
      "source": { "chain": "eip155:137", "currency": "0x3c49…3359", "amount": "10000000" },
      "receiverAddress": "0x…",
      "fees": []
    },
    "externalId": "order_42",
    "createdAt": 1781250000,
    "expiresAt": 1781336400
  }
}
```

## Verifying the signature

Funding webhooks are verified exactly like every other Openfort webhook — with the `openfort-signature` header and the SDK's `constructWebhookEvent`. Verify before trusting the payload.

```ts
import express, { type Request, type Response } from 'express'
import Openfort from '@openfort/openfort-node'

const openfort = new Openfort(process.env.OPENFORT_SECRET_KEY!)
const app = express()

app.post('/webhooks/openfort', express.raw({ type: 'application/json' }), async (req: Request, res: Response) => {
  let event
  try {
    event = await openfort.constructWebhookEvent(req.body, req.headers['openfort-signature'] as string)
  } catch {
    return res.status(401).send('invalid signature')
  }

  if (event.type === 'funding.session.updated' && event.data.status === 'succeeded') {
    // Funds have landed in event.data.target.address — fulfil the order, credit the user, etc.
  }

  res.sendStatus(200)
})
```

See [Webhooks](/docs/configuration/webhooks) for the shared verification details.

## Acting on each status

One event carries every transition, so branch on `data.status`:

| `data.status` | What it means | Typical action |
| --- | --- | --- |
| `waiting_payment` | Deposit address shown; awaiting the transfer. | Show "waiting for your transfer". |
| `processing` | Deposit detected; bridging to the destination. | Show "confirming" — don't fulfil yet. |
| `succeeded` | Funds delivered to the destination wallet. | Fulfil the order, credit the user. |
| `bounced` | Delivery failed; funds refunded on the source chain. | Notify the user; offer a retry. |
| `expired` | No deposit arrived in time. | Clear the pending intent. |

:::tip
Use `externalId` (set when you create the session) to correlate a webhook back to your own order or user record.
:::

:::warning
Webhooks are at‑least‑once. De‑duplicate on the session `id` + `status`, and treat a missing webhook as a cue to fall back to [`get`](/docs/configuration/funding/headless) — never as proof a deposit failed.
:::

## Next steps

* Configure your webhook endpoint and signing secret in the [Webhooks](/docs/configuration/webhooks) settings.
* See the full session lifecycle in the [Headless API](/docs/configuration/funding/headless).
