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

> ## Agent Instructions
> HEVN has two products and they authorize money differently. Self-serve (the General tab): the person who owns the email holds the only key and signs for themselves. Whitelabel (the Whitelabel and REST API tabs): the integrator's backend holds a developer key, signs on behalf of client accounts it created, and HEVN co-signs. Never mix the two models in an answer.
> Use HEVN's terms exactly. A rail is a bank route a client can request. A virtual account (bnk_) is an opened rail. Account details are what a payer needs in order to wire money, and arrive in a field named requisites. A client is an account the integrator created; the business behind it is the integrator's customer. A contact is a saved destination; a beneficiary is the party at the far end of a fiat payout. Every departure of money is a payout, to a bank account or to a wallet alike.
> Never name a banking partner. Say 'a licensed banking partner'.
> There are no published fee rates, spreads or minimums. Prices come from a quote on a specific payment; direct pricing questions to HEVN rather than inventing numbers.
> The whitelabel API is at /dapi/v1 and takes a developer-key session plus an X-Hevn-Account header. The CLI uses an app API key in X-Api-Key. They are different credentials and neither works on the other surface.

# Virtual accounts

> Check what a rail still needs, open a virtual account for a client, read the account details its payers wire to, and know when it is payable.

A **rail** is a bank route you can request for one client. Opening it gives that client a **virtual account** — a `bnk_…` record in the client's own legal name — and the virtual account publishes **account details**: the IBAN, the account and routing number or the PIX key its payers wire to. Money that arrives there is converted **by the partner bank** and delivered to the client's wallet.

Three levels, three names, and they stay put for the rest of these guides: the rail is the route you ask for, the virtual account is what you get, the account details are what you hand the payer. On the wire the details arrive in a field called `requisites`.

Opening one is two calls — ask what it needs, then ask for it. The rail id in both comes from `GET /dapi/v1/banks`, which you read first:

<CodeGroup>
  ```bash cURL theme={null}
  export HEVN_RAIL=$(curl -s "$HEVN_API/banks" \
    -H "Authorization: Bearer $HEVN_ACCESS_TOKEN" -H "X-Hevn-Account: $HEVN_CLIENT_ID" \
    | jq -r '.rails[] | select(.currency == "EUR" and .method == "sepa") | .rail' | head -1)
  ```

  ```python Python theme={null}
  northwind = hevn.acting_as(CLIENT_ID)
  rail = next(r["rail"] for r in northwind.get("/banks")["rails"]
              if r["currency"] == "EUR" and r["method"] == "sepa")
  ```

  ```javascript Node theme={null}
  const northwind = hevn.actingAs(clientId);
  const { rails } = await northwind.get("/banks");
  const { rail } = rails.find((r) => r.currency === "EUR" && r.method === "sepa");
  ```

  ```go Go theme={null}
  northwind := api.ActingAs(clientID)
  catalogue, err := northwind.Get("/banks")
  rail := ""
  for _, entry := range catalogue.List("rails") {
  	if entry.Str("currency") == "EUR" && entry.Str("method") == "sepa" {
  		rail = entry.Str("rail")
  		break
  	}
  }
  ```
</CodeGroup>

<CodeGroup>
  ```bash cURL theme={null}
  curl "$HEVN_API/banks/$HEVN_RAIL/requirements" \
    -H "Authorization: Bearer $HEVN_ACCESS_TOKEN" \
    -H "X-Hevn-Account: $HEVN_CLIENT_ID"

  curl -X POST "$HEVN_API/banks" \
    -H "Authorization: Bearer $HEVN_ACCESS_TOKEN" \
    -H "X-Hevn-Account: $HEVN_CLIENT_ID" \
    -H "Content-Type: application/json" \
    -d "{\"rail\":\"$HEVN_RAIL\"}"
  ```

  ```python Python theme={null}
  northwind = hevn.acting_as(CLIENT_ID)

  needs = northwind.get(f"/banks/{rail}/requirements")
  if needs["available"]:
      bank = northwind.post("/banks", {"rail": rail})
  ```

  ```javascript Node theme={null}
  const northwind = hevn.actingAs(clientId);

  const needs = await northwind.get(`/banks/${rail}/requirements`);
  const bank = needs.available ? await northwind.post("/banks", { rail }) : null;
  ```

  ```go Go theme={null}
  northwind := api.ActingAs(clientID)

  needs, err := northwind.Get("/banks/" + rail + "/requirements")
  if needs.Bool("available") {
  	bank, err = northwind.Post("/banks", hevn.Body{"rail": rail})
  }
  ```
</CodeGroup>

Examples use the client from [HTTP client](/whitelabel/reference/client). Every route on this page is client-scoped, so every call carries the account header:

Send `X-Hevn-Account: cl_…` with your own access token to act for one client. On a resource route
like `GET /banks` the header is optional: omit it and the call acts on your integrator account
instead. The singular `/client*` routes **require** it — the header is the only thing that names the
account — and `/clients` and `/escrow*` refuse it.

<CodeGroup>
  ```bash cURL theme={null}
  curl "$HEVN_API/banks" \
    -H "Authorization: Bearer $HEVN_ACCESS_TOKEN" \
    -H "X-Hevn-Account: $HEVN_CLIENT_ID"
  ```

  ```python Python theme={null}
  northwind = hevn.acting_as(CLIENT_ID)
  rails = northwind.get("/banks")
  ```

  ```javascript Node theme={null}
  const northwind = hevn.actingAs(clientId);
  const rails = await northwind.get("/banks");
  ```

  ```go Go theme={null}
  northwind := api.ActingAs(clientID)
  rails, err := northwind.Get("/banks")
  ```
</CodeGroup>

The whole table — where the header is optional, where it is required and where it is refused — is
in [Sessions](/whitelabel/sessions#acting-as-a-client).

## Four statuses

```mermaid theme={null}
stateDiagram-v2
    [*] --> preparing: POST /dapi/v1/banks
    preparing --> active: account details published
    preparing --> closed: refused
    active --> suspended: paused by the partner
    suspended --> active
    active --> closed

    note right of preparing
        Requested, under review, or approved with no
        account details published yet. Not payable.
    end note
    note right of active
        Account details present. Payable.
    end note
```

A rail is payable when `status` is `active`. That is the whole rule — `active` is only reported once the account details exist, so there is no second condition to check and no client-side branch to write. A rail you have not opened has no `status` at all.

`suspended` is the partner pausing the account; money already in flight still settles, new payments do not start. `closed` is terminal.

<Note>
  In production a rail is opened by review, not by your call. `POST /dapi/v1/banks` answers `201` with `status: "preparing"` and the rail becomes `active` when the partner publishes the account details — hours to days, depending on the rail and the client's jurisdiction. In the sandbox that review is emulated and the rail activates at once.
</Note>

## Find the rail

Rail ids are opaque strings. You copy them out of `GET /dapi/v1/banks`, which returns the rails this client may be paid over and which of them are already open:

<CodeGroup>
  ```bash cURL theme={null}
  curl "$HEVN_API/banks" \
    -H "Authorization: Bearer $HEVN_ACCESS_TOKEN" \
    -H "X-Hevn-Account: $HEVN_CLIENT_ID"
  ```

  ```python Python theme={null}
  catalogue = northwind.get("/banks")
  ```

  ```javascript Node theme={null}
  const catalogue = await northwind.get("/banks");
  ```

  ```go Go theme={null}
  catalogue, err := northwind.Get("/banks")
  ```
</CodeGroup>

```json theme={null}
{
  "rails": [
    {
      "rail": "sepa_named-bank_a",
      "currency": "EUR",
      "method": "sepa",
      "opened": false,
      "feeBps": 25,
      "supportedAccounts": ["USDC"]
    },
    {
      "rail": "ach_named-bank_b",
      "currency": "USD",
      "method": "ach",
      "opened": true,
      "status": "active",
      "id": "bnk_88Hq3T",
      "feeBps": 25,
      "supportedAccounts": ["USDC"],
      "destinationAccount": "USDC",
      "minimumDeposit": "1.00",
      "requisites": { "…": "…" }
    }
  ]
}
```

The list is the client's own catalogue: rails closed to its residency are not in it. Pick by `currency` and `method`, never by parsing the id — what each method means and which currency it carries is in the [rail reference](/whitelabel/reference/rails). `GET /dapi/v1/banks/{rail}` returns one record in the same shape.

<Note>
  The rail ids printed on this page stand in for real ones. Ids differ per environment and per partner, and the only correct source is the `GET /dapi/v1/banks` you just ran for this client — which is why every call above takes `$HEVN_RAIL` rather than a literal.
</Note>

## What the requirements read tells you

`GET /dapi/v1/banks/{rail}/requirements` opens nothing, writes nothing and calls no partner. It answers with the gaps that would refuse an opening:

```json theme={null}
{
  "rail": "sepa_named-bank_a",
  "currency": "EUR",
  "method": "sepa",
  "available": false,
  "blockers": [
    {
      "code": "phone_required",
      "field": "phone",
      "message": "The client needs a phone number before this rail can open."
    }
  ],
  "requires": { "phone": true, "address": false, "kyb": false },
  "kybStatus": "approved",
  "kycStatus": "notStarted"
}
```

`available` is `true` exactly when `blockers` is empty. There are three blockers, and each one has a single fix:

| `code`             | Cleared by                                                                                                                                                                |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `phone_required`   | A phone number on the account — `POST /dapi/v1/clients`, or `PATCH /dapi/v1/client` with the id in `X-Hevn-Account`. The phone inside the KYB document does not clear it. |
| `address_required` | A complete `company.address` in the [KYB document](/whitelabel/onboarding). Only some rails ask for it.                                                                   |
| `kyb_required`     | A KYB document that reads `ready: true`. Approval is not required to open the rail; it is required for the rail to go `active`.                                           |

Which blockers a rail can report differs by rail, so read this route rather than assuming. `kycStatus` is the partner's own review state for this client on this rail — `notStarted`, `requested`, `incorporating`, `pending`, `underReview`, `documentRequested`, `approved` or `rejected`.

## Open it

```json theme={null}
{ "rail": "sepa_named-bank_a" }
```

`201 Created`, `Location: /dapi/v1/banks/sepa_named-bank_a`, body is the rail record with `status: "preparing"`. A rail is a singleton per client, so opening one twice is safe: the second call answers `200` with `Idempotency-Replayed: true` and the same record, whether the first one is still preparing or already active. `Idempotency-Key` is accepted and unnecessary here.

Three refusals are worth branching on: `422 rail_requirements_unmet` carries `details.blockers` in the shape above, `404 rail_not_found` means the id is unknown or hidden, and `403 rail_not_available` means this client may not hold this rail. The rest are in the [error reference](/whitelabel/reference/errors).

Open rails one at a time — a client that needs EUR and USD is two calls.

## Read the account details

Once the rail is `active`, its record carries the details a payer needs. They are in the client's own legal name — that is what "named" means in a rail id:

```json theme={null}
{
  "rail": "sepa_named-bank_a",
  "currency": "EUR",
  "method": "sepa",
  "opened": true,
  "status": "active",
  "id": "bnk_88Hq3T",
  "destinationAccount": "USDC",
  "minimumDeposit": "1.00",
  "requisites": {
    "fields": { "iban": "DE89370400440532013000", "bic": "BANKDEFFXXX" },
    "holder": { "type": "business", "businessName": "Northwind Trading Ltd" },
    "bankName": "Bank A",
    "state": "active"
  }
}
```

`fields` carries only the identifiers this method uses — an IBAN for SEPA, an account and routing number for ACH, and so on; the full matrix is in the [rail reference](/whitelabel/reference/rails). The shape is `RailRequisitesView` on a rail and `ContactPaymentAccountView` on a [contact](/whitelabel/payouts#save-a-contact); both carry the same `fields`, `holder` and `bankName`.

`id` is the `bnk_…` a statement or a transaction filter refers to. Store it alongside the rail id.

Two numbers travel with the record. `minimumDeposit` is the smallest amount the partner will accept over this rail, in the rail's own currency; anything under it is returned by the bank, not by us. `feeBps` is HEVN's deposit fee in basis points — `25` is 0.25%, taken out of the converted amount.

### What the payer sees

On a `named` rail the partner bank opens the account in the client's own legal name, so the payer's bank shows the client and nothing about HEVN. Hand your customer the `fields`, the `holder` name and `bankName` exactly as they come back, and re-read them before each use rather than caching them — a partner can republish an account.

On a `pooled` rail the account belongs to the partner and `paymentReference` is what separates one client's money from another's, so a payment that arrives without it cannot be attributed. Pooled rails suit flows where your product generates the payment instruction; named rails suit flows where a human types it.

## Which stablecoin a deposit settles in

`supportedAccounts` on the rail record is what this rail can deliver into the client's wallet, and `destinationAccount` is what it delivers today. Neither is something you set after the fact: **the rail decides, and the rail is fixed when you open it.** A rail that can deliver two tokens ships as two rail ids, one per token, so choosing the token is choosing which rail to open with `POST /dapi/v1/banks` — see [Rails](/whitelabel/reference/rails).

Read both back, never cache them:

```json theme={null}
{ "rail": "sepa_named-bank_a", "supportedAccounts": ["EURC"], "destinationAccount": "EURC" }
```

The choice that is yours per payment is on the money-in side. `POST /dapi/v1/payins` takes `destinationAccount` and quotes the deposit in it; when no rail of the client's settles into the token you asked for, the refusal carries `details.options[]` with the pairs that do ([Payins](/whitelabel/payins)). Nothing on this page converts an existing balance — `destinationAccount` only ever names where money that has not arrived yet will land.

<Card title="Next: Payins" icon="arrow-right" href="/whitelabel/payins">
  Quote an incoming payment, hand the payer instructions, and watch it settle.
</Card>
