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

> ## Agent Instructions
> HEVN is payments software: an API and app over a Base smart wallet. HEVN is not a bank and holds no key to the wallet. Virtual accounts, fiat payins and fiat payouts are provided by partner banks, which hold fiat while it is in transit. Self-serve (the General tab): the person who controls the email signs for the wallet. Developers integrate /dapi/v1 in one of two ways. HEVN Connect (recommended, the HEVN Connect tab): the integrator's customer creates or links a HEVN account in a HEVN-hosted sign-in window on the integrator's site, the integrator's backend reads the linked account and books its payouts, and the account holder approves each payout in that window. External wallets (the External wallets tab, enabled per integrator): the integrator creates a client through the API, registers a wallet address the customer holds as an external account, and requests virtual accounts that deliver to that address. The customer's own wallet sends each transfer that funds a payout. The developer key signs for the integrator's wallet and its sub accounts and cannot sign for a client's wallet.
> Use HEVN's terms as written. A product is the region a client requests. A rail is the bank route HEVN picks for the product. A virtual account (bnk_) is the record a partner bank opens for a client. Account details are what a payer needs to send a bank transfer, returned in a field named requisites. A client (cl_) is a customer's account: linked to the integrator through Sign in with HEVN (POST /dapi/v1/connect/sessions, then POST /dapi/v1/clients/link), or created with POST /dapi/v1/clients for an external-wallets integration. An external account (acc_, kind external) is a wallet address the client holds and signs for. A sub account (acc_, kind sub) is a further wallet of the integrator's own account on Base, spent with the developer key. Write 'sub account' as two words. A contact is a saved destination. A beneficiary is the party that receives a fiat payout. Money leaving an account is a payout, whether it goes to a bank account or to a wallet.
> Do not name a bank, payment institution, card issuer, identity-verification vendor or stablecoin issuer. Say 'the partner bank'. Do not describe HEVN as licensed, regulated or insured.
> HEVN publishes no fee rates, spreads or minimums. Prices come from a quote on a specific payment. Direct pricing questions to HEVN and do not invent numbers.
> The API is at /dapi/v1 and takes a developer-key session plus an X-Hevn-Account header. Developer keys have an IP allowlist and no scopes. Remote transfers are at /api/v1/remote_spend_limit and take a remote transfer key (rsl_) in X-Api-Key, bounded by an onchain spend permission the account holder grants. The two credentials are separate and neither works on the other API. The HEVN CLI is discontinued.

# Sign in with HEVN

> Open the HEVN sign-in window to link a customer's account, approve a payout, or verify an individual.

Sign in with HEVN is a HEVN-hosted popup. In it your customer links a HEVN account to your integrator account, approves a payout, or verifies their identity. Create a session with `POST /dapi/v1/connect/sessions` and open its `url` in the popup.

The holder signs in with Google or a one-time email code. The window shows no separate consent screen and no HEVN product screens.

## Configure the window

In the HEVN app, open **Settings → Partner program → Developer**. In the **Sign-in window** card, **Allowed sites** lists the origins the window may return to.

* An origin is `https://host`, or `http://localhost:<port>` and `http://127.0.0.1:<port>` in development, with no path or query.
* The list holds up to 20 origins. An empty list allows no session.
* Removing an origin stops the sessions already issued for it.
* HEVN sets the title, logo, colours and theme on request. The API and the session URL cannot change them.

Serve one static [callback page](#the-callback-page) from an allowed origin.

Until HEVN [activates your integrator account](https://docs.gethevn.com/developers/sandbox.md#what-hevn-must-enable), `POST /dapi/v1/connect/sessions` and `POST /dapi/v1/clients/link` return `403 client_creation_not_enabled`.

## Link an account

### 1. Open the popup

Open an empty window in the click handler, before any `await`, then navigate it to the session `url`. Browsers block a popup opened later.

```javascript Browser theme={null}
button.addEventListener("click", async () => {
  const popup = window.open("about:blank", "hevn-connect", "popup,width=460,height=720");
  if (!popup) return showError("Allow pop-ups for this site to sign in.");

  const { url } = await (await fetch("/hevn/link/start", { method: "POST" })).json();
  const result = await hevnResult(url, popup);
  if (result.error) return showError(result.error);

  await fetch("/hevn/link/finish", {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({ code: result.code, state: result.state }),
  });
});
```

### 2. Create a session

Call `POST /dapi/v1/connect/sessions` from your server with your access token, without `X-Hevn-Account`. The `action` values and field rules are in [Create a sign-in window session](https://docs.gethevn.com/api-reference/accounts/create-a-sign-in-window-session.md).

```bash cURL theme={null}
curl -X POST "$HEVN_API/connect/sessions" \
  -H "Authorization: Bearer $HEVN_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"action":"link","redirectUri":"https://app.example.com/hevn/callback"}'
```

```python Python theme={null}
session = hevn.post("/connect/sessions", {
    "action": "link",
    "redirectUri": "https://app.example.com/hevn/callback",
})
```

```javascript Node theme={null}
const session = await hevn.post("/connect/sessions", {
  action: "link",
  redirectUri: "https://app.example.com/hevn/callback",
});
```

```go Go theme={null}
session, err := api.Post("/connect/sessions", hevn.Body{
	"action":      "link",
	"redirectUri": "https://app.example.com/hevn/callback",
})
```

```json Response 201 theme={null}
{
  "url": "https://sandbox.gethevn.com/connect?session=eyJhbGciOiJFUzI1NiIs…",
  "state": "pQ8v2Zr0cJ3mX7tLkN1wA5yHs9dE4fBq",
  "expiresAt": "2026-09-17T10:14:11Z"
}
```

HEVN signs `url` and binds it to your integrator account, the action, the payout or client, and `redirectUri`. It also binds a one-time nonce, returned as `state`.

### 3. Bind the session to this browser

Store `state` in an HttpOnly cookie when you create the session. Accept a code only when the browser returns the same `state` with that cookie.

```javascript Node theme={null}
app.post("/hevn/link/start", async (req, res) => {
  const session = await hevn.post("/connect/sessions", {
    action: "link",
    redirectUri: "https://app.example.com/hevn/callback",
  });
  res.cookie("hevn_link", sign({ state: session.state }), {
    httpOnly: true, secure: true, sameSite: "lax", path: "/hevn/link", maxAge: 10 * 60_000,
  });
  res.json({ url: session.url });
});
```

### 4. Exchange the code

Exchange the returned `code` from your server with `POST /dapi/v1/clients/link`. Its validity and retry rules are in [Connect an existing account](https://docs.gethevn.com/api-reference/accounts/connect-an-existing-account.md).

```bash cURL theme={null}
curl -X POST "$HEVN_API/clients/link" \
  -H "Authorization: Bearer $HEVN_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"code":"eyJhbGciOiJFUzI1NiIs…"}'
```

```python Python theme={null}
client = hevn.post("/clients/link", {"code": code})
```

```javascript Node theme={null}
app.post("/hevn/link/finish", async (req, res) => {
  const attempt = verify(req.cookies.hevn_link);
  res.clearCookie("hevn_link", { path: "/hevn/link" });
  if (!attempt || !timingSafeEqual(attempt.state, req.body.state)) {
    return res.status(400).json({ code: "state_mismatch" });
  }
  const client = await hevn.post("/clients/link", { code: req.body.code });
  req.session.clientId = client.id;
  res.json({ signedIn: true });
});
```

```go Go theme={null}
client, err := api.Post("/clients/link", hevn.Body{"code": code})
```

```json Response 200 theme={null}
{ "id": "cl_7YQ2Kf3mN8", "status": "ready", … }
```

The response is the [client record](https://docs.gethevn.com/developers/clients.md). Store `id` on your server and send it in `X-Hevn-Account`. Do not accept it from the browser.

On `400 link_code_invalid` or `410 link_code_expired`, start a new sign-in. Do not retry `409 client_already_linked`.

### Pass a known email

To sign the holder in with an email you hold, add `login_hint` to the session URL. Do it before you navigate the popup.

```javascript Browser theme={null}
const opened = new URL(url);
opened.searchParams.set("login_hint", "jane@acme.example");
popup.location.href = opened.toString();
```

The window sends a one-time code to that address, and the holder cannot change it. `login_hint` is not signed and does not prove who signed in. `POST /dapi/v1/clients/link` returns the account that did.

## The callback page

The window finishes by navigating to `redirectUri` with `state` and the result in the query string.

| Session | Query parameters |
| - | - |
| `link` | `code` |
| `sign` | `status`, `payout`, `reference` |
| `verify` | `status` |
| Refused | `error` |

The callback page posts the query to the opener over a `BroadcastChannel`, because `window.opener` is `null` after Google sign-in.

```html callback.html theme={null}
<!doctype html>
<meta charset="utf-8">
<title>Signing in…</title>
<script>
  const result = Object.fromEntries(new URLSearchParams(location.search));
  if (result.state) {
    const channel = new BroadcastChannel("hevn-connect");
    channel.postMessage({ state: result.state, result });
    channel.close();
    window.close();
  }
</script>
```

The opener resolves on the result whose `state` matches its session.

```javascript Browser theme={null}
function hevnResult(url, popup, timeoutMs = 10 * 60_000) {
  const token = new URL(url).searchParams.get("session");
  const body = token.split(".")[1].replace(/-/g, "+").replace(/_/g, "/");
  const { nonce } = JSON.parse(atob(body.padEnd(Math.ceil(body.length / 4) * 4, "=")));
  const channel = new BroadcastChannel("hevn-connect");
  return new Promise((resolve, reject) => {
    const timer = setTimeout(() => { channel.close(); reject(new Error("timeout")); }, timeoutMs);
    channel.onmessage = ({ data }) => {
      if (data?.state !== nonce) return;
      clearTimeout(timer);
      channel.close();
      resolve(data.result);
    };
    popup.location.href = url;
  });
}
```

A `BroadcastChannel` reaches pages of one origin, so serve the callback page from the opener's origin. Matching `state` in the browser routes the result and does not authenticate it. The cookie check in [step 3](#3-bind-the-session-to-this-browser) does.

> [!NOTE]
>
> [hevn-inc/dapi-sdk](https://github.com/hevn-inc/dapi-sdk) has this browser code as `HevnConnect`. The repository is a reference and is not a supported package.

## Who can be linked

An account can be linked only if it was created in the sign-in window. The window signs out any HEVN session in the browser before each `link` sign-in, so the holder chooses the account.

| The person signing in | Result |
| - | - |
| Has no HEVN account | A new account and a `code`. |
| Is already linked to you | A new `code` for the same client. Use this to log a returning customer in. |
| Has a HEVN account not created in the window | Refused with `already_registered`. The window offers to open HEVN. |
| Is linked to another integrator | Refused with `already_linked`. |

The window names your product. It tells the holder that you can see balances, request virtual accounts and prepare transfers. It also says that the holder approves each transfer in the window.

## Approve a payout

A payout booked for a linked client returns `authorization: "client"`, and the holder signs it. Create a `sign` session with the call from [step 2](#2-create-a-session) and open the popup as for a link.

```json POST /dapi/v1/connect/sessions theme={null}
{
  "action": "sign",
  "payoutId": "po_6d21…",
  "redirectUri": "https://app.example.com/hevn/callback"
}
```

The route returns `404 payout_not_found` when the payout does not belong to a client linked to you. It returns `409 payout_not_awaiting_signature` when the payout is no longer `awaitingSignature`.

The holder signs in, or reuses a HEVN session open in that browser. The window shows the amount, the contact's name and who requested the payout. The holder's wallet signs the debit.

The callback receives `status=submitted`, `payout=po_…` and, when the debit was sent, `reference`. If the payout was no longer waiting when the window opened, nothing is signed. The callback receives its current `status`.

Do not act on the redirect. Read `GET /dapi/v1/payouts/{payoutId}` with the client in `X-Hevn-Account` and follow it to `settled` ([Money movement](https://docs.gethevn.com/connect/money-movement.md#track-the-payout)).

## Verify an individual

A linked individual verifies their identity in a `verify` session. HEVN does not issue a verification link to you. A business client verifies through [KYB](https://docs.gethevn.com/developers/clients.md#write-the-document).

Create a `verify` session when a virtual account has the [`verification_required` blocker](https://docs.gethevn.com/developers/virtual-accounts#what-still-blocks-it), and open the popup as for a link.

```bash cURL theme={null}
curl -X POST "$HEVN_API/connect/sessions" \
  -H "Authorization: Bearer $HEVN_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"action":"verify","clientId":"cl_7YQ2Kf3mN8","redirectUri":"https://app.example.com/hevn/callback"}'
```

```python Python theme={null}
session = hevn.post("/connect/sessions", {
    "action": "verify",
    "clientId": client_id,
    "redirectUri": "https://app.example.com/hevn/callback",
})
```

```javascript Node theme={null}
const session = await hevn.post("/connect/sessions", {
  action: "verify",
  clientId,
  redirectUri: "https://app.example.com/hevn/callback",
});
```

```go Go theme={null}
session, err := api.Post("/connect/sessions", hevn.Body{
	"action":      "verify",
	"clientId":    clientID,
	"redirectUri": "https://app.example.com/hevn/callback",
})
```

The route returns `404 client_not_found` for a client that does not exist or is not linked to you. It returns `409 verification_not_applicable` for a business client. For a verified client, the window says so and returns `status=approved`.

The identity check runs in the window. When the holder finishes or closes it, the callback receives `status`.

| `status` | Meaning | Next |
| - | - | - |
| `approved` | Verified. | Read the client. |
| `inReview` | Submitted and under review. | Poll the client. |
| `actionRequired` | Something is missing. | Open a new `verify` session when the holder is ready. |
| `rejected` | Refused. | n/a |
| `notStarted` | The holder left before submitting. | Open a new `verify` session. |

A review can finish after the window closes. Poll [`verifiedAt`](https://docs.gethevn.com/developers/clients) on `GET /dapi/v1/client`, or poll `GET /dapi/v1/banks/{product}`.

In the sandbox the window accepts test documents. In automated tests, verify an individual with [`POST /dapi/v1/sandbox/kyb/status`](https://docs.gethevn.com/developers/sandbox#decide-a-kyb-application).

## Errors

The window returns a refusal as `error` on the redirect. API errors are in the [error reference](https://docs.gethevn.com/developers/reference/errors.md).

A session that is invalid or expired when it opens sends no result. The window shows "This link has expired or is invalid". Stop waiting at `expiresAt` and close the popup.

| `error` | Cause | Next |
| - | - | - |
| `access_denied` | The holder cancelled. | Let them try again. |
| `already_registered` | The Google account has a HEVN account not created in the window. | Ask the customer to use another Google account. |
| `already_linked` | The account is linked to another integrator. | Do not retry. |
| `account_mismatch` | `verify`: the holder signed in to an account other than the session's client. | Ask the holder to use the linked account. |
| `not_found` | `sign`: the holder does not hold the payout, or it is not linked to you. Usually the wrong Google account. | Ask the holder to use the linked account. |
| `unsupported` | The payout cannot be approved in the window. | See [Approve the payout](https://docs.gethevn.com/connect/money-movement.md#approve-the-payout). |
| `expired` | The session expired while the holder was in the window. | Create a new session. |
| `failed` | Any other failure. | Create a new session. |

> [card] [Next: Money movement](https://docs.gethevn.com/connect/money-movement.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.