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

# Beneficiary events

> The two beneficiary.* webhook events: identity collection completing, and a bank account being registered.

Two `beneficiary.*` events report that a Beneficiary — or one of their bank accounts —
finished registering. They share the
[envelope](/api-reference/webhook-subscriptions#payload-envelope) and the
[ordering contract](/api-reference/webhook-subscriptions#ordering-and-dedup) every Cleff
webhook uses; subscribe to them by name in `enabled_events`.

Beneficiary events carry no Payout context — no `payout_id`, no `amount`, no `rail_ref`.
They are scoped to the Beneficiary, and order per Beneficiary by the same
`(occurred_at, sequence)` rule the Payout events use.

The two are deliberately separate concerns, split by ADR-0030: identity is a
Beneficiary-level fact, while a bank account is a per-account fact and one Beneficiary
can register several accounts over their life.

## `beneficiary.details_submitted`

Fires when a Beneficiary completes an **identity** collection link — the Cleff-hosted
form behind a `full_onboarding` invite. Beneficiaries do not authenticate to Cleff and
have no portal, so this webhook is the only way a Business learns that a Beneficiary
finished: subscribe to it rather than polling `GET /v1/beneficiaries/{id}`.

**Identity only.** ADR-0030 narrowed this event to identity collection and moved bank
details to `beneficiary.bank_account.registered`. A `bank_details`-scoped link fires only
`beneficiary.bank_account.registered`; a `full_onboarding` submit fires both, because an
account was registered *and* identity was completed.

| Field            | Type     | Meaning                      |
| ---------------- | -------- | ---------------------------- |
| `beneficiary_id` | `string` | Cleff Beneficiary identifier |
| `status`         | `string` | Always `"details_submitted"` |

Re-fires once per completed identity submission. A fresh `full_onboarding` link issued to
an already-onboarded Beneficiary re-collects their identity and fires the event again, so
treat it as an update rather than a first-time signal.

```json theme={null}
{
  "id": "evt_…",
  "type": "beneficiary.details_submitted",
  "api_version": "2026-05-27",
  "occurred_at": "2026-05-26T12:20:32.000Z",
  "created_at": "2026-05-26T12:20:33.000Z",
  "sequence": 10473,
  "data": {
    "beneficiary_id": "ben_…",
    "status": "details_submitted"
  }
}
```

## `beneficiary.bank_account.registered`

Fires on a bank-account registration through any channel: a Business adding payment
details from the dashboard, a Beneficiary submitting through a Cleff-hosted collection
link, or a direct API registration. Channel-agnostic is the point — the subscriber learns
that a payable destination now exists, never how it was sourced. How the account was
collected stays internal and is not on the wire. Bulk import is the one deliberate
exception, noted below.

| Field             | Type     | Meaning                                                                                      |
| ----------------- | -------- | -------------------------------------------------------------------------------------------- |
| `bank_account_id` | `string` | Cleff bank-account identifier; the same id Payouts name                                      |
| `beneficiary_id`  | `string` | Cleff Beneficiary identifier the account belongs to                                          |
| `last4`           | `string` | Masked display value — trailing 4 of the IBAN or account number; a Pix key masks by key type |
| `country`         | `string` | ISO 3166-1 alpha-2 country of the account                                                    |
| `currency`        | `string` | ISO 4217 code the account is denominated in                                                  |

Fires once per registered account, so a Beneficiary with several accounts produces
several events. Editing an existing account does not re-fire it.

```json theme={null}
{
  "id": "evt_…",
  "type": "beneficiary.bank_account.registered",
  "api_version": "2026-05-27",
  "occurred_at": "2026-05-26T12:20:32.000Z",
  "created_at": "2026-05-26T12:20:33.000Z",
  "sequence": 10474,
  "data": {
    "bank_account_id": "ba_9a04…",
    "beneficiary_id": "ben_…",
    "last4": "6789",
    "country": "US",
    "currency": "USD"
  }
}
```

<Note>
  `POST /v1/beneficiaries/import` deliberately fires **no** `beneficiary.bank_account.registered`
  for its rows. The response already carries the complete result for every row synchronously, so the
  caller holds everything the webhook would have delivered. A consumer that builds its state purely
  from the event stream will not see imported accounts — read them from `results` instead. See
  [Importing a verified base](/api-reference/beneficiaries#importing-a-verified-base).
</Note>
