> ## 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.

# HTTP client

> Reference HTTP client in Python, Node and Go: key loading, login, signing, retries and polling.

The guides call one small HTTP client, shown here in Python, Node and Go. Copy it into your project, then [log in](#log-in) with `POST /dapi/v1/auth/challenge` and `POST /dapi/v1/auth/token`.

> [!NOTE]
>
> A fuller Node version is at [hevn-inc/dapi-sdk](https://github.com/hevn-inc/dapi-sdk). It also checks that the payload you sign matches the payment you requested. The package is unsupported, so use it as a model for your client.

## Surface

| Task | Python | Node | Go |
| - | - | - | - |
| Act for a client, or for yourself | `hevn.acting_as(client_id)` | `hevn.actingAs(clientId)` | `api.ActingAs(clientID)` |
| Read | `hevn.get(path, params={…})` | `hevn.get(path, {…})` | `api.Get(path, hevn.Query{…})` |
| Write | `hevn.post(path, body, idempotency_key="…")` | `hevn.post(path, body, { idempotencyKey })` | `api.Post(path, hevn.Body{…}, hevn.IdempotencyKey("…"))` |
| Read a response field | `payout["approval"]["payload"]` | `payout.approval.payload` | `payout.Str("approval.payload")` |
| Walk a response list | `page["items"]` | `page.items` | `page.List("items")` |
| Sign an approval | `key.sign_payload(b64)` | `key.signPayload(b64)` | `key.SignPayload(b64)` |
| Name the signing key | `key.public_key` | `key.publicKey` | `key.PublicKey` |
| Confirm to settlement | `hevn.confirm_until_settled(path, sig)` | `hevn.confirmUntilSettled(path, sig)` | `api.ConfirmUntilSettled(path, sig)` |
| Poll a read | `poll_until(read, done)` | `pollUntil(read, done)` | `hevn.PollUntil(read, done, deadline)` |
| Download an export | `hevn.download(path, params, into)` | `hevn.download(path, params, into)` | `api.Download(path, hevn.Query{…}, into)` |

The client reads `HEVN_API`, `HEVN_KEY_PEM` and `HEVN_EMAIL`, listed in [Sandbox](https://docs.gethevn.com/developers/sandbox.md). `HEVN_API` must end in `/dapi/v1`, for example `https://sandbox-api.hevn.finance/dapi/v1`. The old `/v1` base returns `404` with no redirect, so check the suffix at start-up.

> [!NOTE]
>
> In the Go tabs `api` is the client variable and `hevn` is the package. A variable named `hevn` shadows the package and the file does not compile.

## Bind an account

`acting_as` returns a bound copy of the HTTP client that sends `X-Hevn-Account` on each call. [Acting as a client](https://docs.gethevn.com/developers/authentication.md#acting-as-a-client) lists where the header is optional, required and refused.

The `/client*` routes require the header. To read your account, bind your `cl_…` id, the `userId` returned by login.

```python Python theme={null}
me = hevn.acting_as(os.environ["HEVN_USER_ID"])   # the userId returned by login
mine = me.get("/client/balance")
```

```javascript Node theme={null}
const me = hevn.actingAs(process.env.HEVN_USER_ID);   // the userId returned by login
const mine = await me.get("/client/balance");
```

```go Go theme={null}
me := api.ActingAs(os.Getenv("HEVN_USER_ID")) // the userId returned by login
mine, err := me.Get("/client/balance")
```

## Install

```bash Python theme={null}
python -m venv .venv && source .venv/bin/activate
pip install cryptography httpx
```

```bash Node theme={null}
# Node 20 or newer. Signing and HTTP use node:crypto and global fetch.
node --version
npm init --yes
```

```bash Go theme={null}
go mod init example.com/hevn
# Signing and HTTP use the standard library only: crypto/ecdsa, encoding/pem, net/http.
```

## Load the developer key

```python Python theme={null}
import base64, os
from cryptography.hazmat.primitives import serialization


def load_key():
    with open(os.environ["HEVN_KEY_PEM"], "rb") as handle:
        return serialization.load_pem_private_key(handle.read(), password=None)


def public_key_b64(key) -> str:
    spki = key.public_key().public_bytes(
        serialization.Encoding.DER,
        serialization.PublicFormat.SubjectPublicKeyInfo,
    )
    return base64.b64encode(spki).decode()
```

```javascript Node theme={null}
import { createPrivateKey, createPublicKey } from "node:crypto";
import { readFileSync } from "node:fs";

export function loadKey() {
  return createPrivateKey(readFileSync(process.env.HEVN_KEY_PEM));
}

export function publicKeyB64(key) {
  return createPublicKey(key).export({ type: "spki", format: "der" }).toString("base64");
}
```

```go Go theme={null}
func LoadKey() (*ecdsa.PrivateKey, error) {
	armored, err := os.ReadFile(os.Getenv("HEVN_KEY_PEM"))
	if err != nil {
		return nil, err
	}
	block, _ := pem.Decode(armored)
	parsed, err := x509.ParsePKCS8PrivateKey(block.Bytes)
	if err != nil {
		return nil, err
	}
	key, ok := parsed.(*ecdsa.PrivateKey)
	if !ok {
		return nil, errors.New("hevn: not a P-256 key")
	}
	return key, nil
}

func PublicKeyB64(key *ecdsa.PrivateKey) (string, error) {
	spki, err := x509.MarshalPKIXPublicKey(&key.PublicKey)
	if err != nil {
		return "", err
	}
	return base64.StdEncoding.EncodeToString(spki), nil
}
```

## Sign bytes

Signatures are ECDSA P-256 over SHA-256, DER-encoded, then base64. See [Signature format](https://docs.gethevn.com/developers/signing.md#signature-format).

```python Python theme={null}
import base64
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import ec


def sign_bytes(key, message: bytes) -> str:
    """base64(DER ECDSA P-256 / SHA-256) over these bytes."""
    return base64.b64encode(key.sign(message, ec.ECDSA(hashes.SHA256()))).decode()


def sign_payload(key, payload_b64: str) -> str:
    return sign_bytes(key, base64.b64decode(payload_b64))
```

```javascript Node theme={null}
import { sign } from "node:crypto";

export function signBytes(key, message) {
  return sign("sha256", message, { key, dsaEncoding: "der" }).toString("base64");
}

export function signPayload(key, payloadB64) {
  return signBytes(key, Buffer.from(payloadB64, "base64"));
}
```

```go Go theme={null}
func SignBytes(key *ecdsa.PrivateKey, message []byte) (string, error) {
	digest := sha256.Sum256(message)
	der, err := ecdsa.SignASN1(rand.Reader, key, digest[:])
	if err != nil {
		return "", err
	}
	return base64.StdEncoding.EncodeToString(der), nil
}

func SignPayload(key *ecdsa.PrivateKey, payloadB64 string) (string, error) {
	message, err := base64.StdEncoding.DecodeString(payloadB64)
	if err != nil {
		return "", err
	}
	return SignBytes(key, message)
}
```

## Wrap the key

`Key` binds the two signing functions and the public key to one loaded key.

```python Python theme={null}
class Key:
    def __init__(self):
        self.raw = load_key()                     # the loaded key login() takes
        self.public_key = public_key_b64(self.raw)

    def sign_bytes(self, message: bytes) -> str:
        return sign_bytes(self.raw, message)

    def sign_payload(self, payload_b64: str) -> str:
        return sign_payload(self.raw, payload_b64)
```

```javascript Node theme={null}
export class Key {
  constructor() {
    this.raw = loadKey();                      // the loaded key login() takes
    this.publicKey = publicKeyB64(this.raw);
  }

  signBytes(message) {
    return signBytes(this.raw, message);
  }

  signPayload(payloadB64) {
    return signPayload(this.raw, payloadB64);
  }
}
```

```go Go theme={null}
type Key struct {
	raw       *ecdsa.PrivateKey // the loaded key Login() takes
	PublicKey string
}

func NewKey() (*Key, error) {
	key, err := LoadKey()
	if err != nil {
		return nil, err
	}
	public, err := PublicKeyB64(key)
	return &Key{raw: key, PublicKey: public}, err
}

func (k *Key) SignBytes(message []byte) (string, error) { return SignBytes(k.raw, message) }
func (k *Key) SignPayload(payloadB64 string) (string, error) {
	return SignPayload(k.raw, payloadB64)
}
```

## Log in

Token lifetimes are in [Sessions](https://docs.gethevn.com/developers/authentication.md#sessions).

```python Python theme={null}
import os, secrets, time, httpx


def login(key) -> dict:
    api, email = os.environ["HEVN_API"], os.environ["HEVN_EMAIL"]
    nonce = secrets.randbits(48)
    expiry = int(time.time() * 1000) + 120_000
    proof = f"hevn-developer-key-login:{email}:{nonce}:{expiry}".encode()

    started = httpx.post(f"{api}/auth/challenge", json={
        "email": email,
        "publicKey": public_key_b64(key),
        "nonce": nonce,
        "requestExpiry": expiry,
        "signature": sign_bytes(key, proof),
    })
    started.raise_for_status()
    challenge = started.json()

    minted = httpx.post(f"{api}/auth/token", json={
        "challengeId": challenge["challengeId"],
        "signature": sign_payload(key, challenge["payload"]),
    })
    minted.raise_for_status()
    return minted.json()  # accessToken, refreshToken, expiresIn, userId
```

```javascript Node theme={null}
export async function login(key) {
  const api = process.env.HEVN_API;
  const email = process.env.HEVN_EMAIL;
  const nonce = Number(process.hrtime.bigint() % 281474976710656n);
  const expiry = Date.now() + 120_000;
  const proof = Buffer.from(`hevn-developer-key-login:${email}:${nonce}:${expiry}`);

  const post = async (path, body) => {
    const response = await fetch(`${api}${path}`, {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify(body),
    });
    if (!response.ok) throw new Error(`${path} ${response.status} ${await response.text()}`);
    return response.json();
  };

  const challenge = await post("/auth/challenge", {
    email,
    publicKey: publicKeyB64(key),
    nonce,
    requestExpiry: expiry,
    signature: signBytes(key, proof),
  });

  return post("/auth/token", {
    challengeId: challenge.challengeId,
    signature: signPayload(key, challenge.payload),
  });
}
```

```go Go theme={null}
func Login(key *ecdsa.PrivateKey) (Tokens, error) {
	api, email := os.Getenv("HEVN_API"), os.Getenv("HEVN_EMAIL")
	nonce := rand.Int63n(1 << 48)
	expiry := time.Now().Add(2 * time.Minute).UnixMilli()

	proof := fmt.Sprintf("hevn-developer-key-login:%s:%d:%d", email, nonce, expiry)
	proofSignature, err := SignBytes(key, []byte(proof))
	if err != nil {
		return Tokens{}, err
	}
	publicKey, err := PublicKeyB64(key)
	if err != nil {
		return Tokens{}, err
	}

	var challenge struct{ ChallengeID, Payload string }
	if err := PostJSON(api+"/auth/challenge", map[string]any{
		"email": email, "publicKey": publicKey, "nonce": nonce,
		"requestExpiry": expiry, "signature": proofSignature,
	}, &challenge); err != nil {
		return Tokens{}, err
	}

	payloadSignature, err := SignPayload(key, challenge.Payload)
	if err != nil {
		return Tokens{}, err
	}
	var tokens Tokens
	err = PostJSON(api+"/auth/token", map[string]any{
		"challengeId": challenge.ChallengeID, "signature": payloadSignature,
	}, &tokens)
	return tokens, err
}
```

## Hold the session

`Session` stores the tokens and refreshes the access token 60 seconds before it expires, or on demand.

```python Python theme={null}
class Session:
    def __init__(self, key):
        self.key = key
        self._tokens, self._expires_at = None, 0.0

    def token(self, refresh: bool = False) -> str:
        if refresh or self._tokens is None or time.time() > self._expires_at - 60:
            self._tokens = login(self.key.raw) if self._tokens is None else self._refresh()
            self._expires_at = time.time() + self._tokens["expiresIn"]
        return self._tokens["accessToken"]

    def _refresh(self) -> dict:
        response = httpx.post(
            f"{os.environ['HEVN_API']}/auth/refresh",
            json={},
            headers={"Authorization": f"Bearer {self._tokens['refreshToken']}"},
        )
        response.raise_for_status()
        return {**self._tokens, **response.json()}
```

```javascript Node theme={null}
export class Session {
  constructor(key) {
    this.key = key;
    this.tokens = null;
    this.expiresAt = 0;
  }

  async token(refresh = false) {
    if (refresh || !this.tokens || Date.now() > this.expiresAt - 60_000) {
      this.tokens = this.tokens ? await this.refresh() : await login(this.key.raw);
      this.expiresAt = Date.now() + this.tokens.expiresIn * 1000;
    }
    return this.tokens.accessToken;
  }

  async refresh() {
    const response = await fetch(`${process.env.HEVN_API}/auth/refresh`, {
      method: "POST",
      headers: {
        authorization: `Bearer ${this.tokens.refreshToken}`,
        "content-type": "application/json",
      },
      body: "{}",
    });
    if (!response.ok) throw new Error(`refresh ${response.status}`);
    return { ...this.tokens, ...(await response.json()) };
  }
}
```

```go Go theme={null}
// Tokens is the response of /auth/token and /auth/refresh. The refresh call
// omits refreshToken, so the zero value keeps the one you already hold.
type Tokens struct {
	AccessToken  string `json:"accessToken"`
	RefreshToken string `json:"refreshToken"`
	ExpiresIn    int    `json:"expiresIn"`
	UserID       string `json:"userId"`
}

type Session struct {
	Key       *Key
	tokens    Tokens
	expiresAt time.Time
	mu        sync.Mutex
}

func (s *Session) Token(refresh bool) (string, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	if !refresh && s.tokens.AccessToken != "" && time.Now().Before(s.expiresAt.Add(-time.Minute)) {
		return s.tokens.AccessToken, nil
	}
	tokens, err := s.mint()
	if err != nil {
		return "", err
	}
	s.tokens = tokens
	s.expiresAt = time.Now().Add(time.Duration(tokens.ExpiresIn) * time.Second)
	return s.tokens.AccessToken, nil
}

func (s *Session) mint() (Tokens, error) {
	if s.tokens.RefreshToken == "" {
		return Login(s.Key.raw)
	}
	var minted Tokens
	err := PostJSON(os.Getenv("HEVN_API")+"/auth/refresh", map[string]any{},
		&minted, Bearer(s.tokens.RefreshToken))
	minted.RefreshToken = s.tokens.RefreshToken // the refresh call does not re-issue it
	return minted, err
}

// PostJSON POSTs a JSON body and decodes the response. opts set headers such as
// the bearer. Login (above) uses it without a bearer, because neither login
// call takes one.
func PostJSON(url string, body map[string]any, out any, opts ...func(*http.Request)) error {
	encoded, err := json.Marshal(body)
	if err != nil {
		return err
	}
	request, err := http.NewRequest("POST", url, bytes.NewReader(encoded))
	if err != nil {
		return err
	}
	request.Header.Set("Content-Type", "application/json")
	for _, apply := range opts {
		apply(request)
	}
	response, err := http.DefaultClient.Do(request)
	if err != nil {
		return err
	}
	defer response.Body.Close()
	if response.StatusCode >= 400 {
		payload, _ := decode(response)
		return asAPIError(response.StatusCode, payload)
	}
	return json.NewDecoder(response.Body).Decode(out)
}

func Bearer(token string) func(*http.Request) {
	return func(r *http.Request) { r.Header.Set("Authorization", "Bearer "+token) }
}
```

## Send a request

`call` raises the [error envelope](https://docs.gethevn.com/developers/reference/errors.md) as a typed error, waits `Retry-After` on a `429`, refreshes the token once on a `401`, and adds `X-Hevn-Account` when the client is bound.

```python Python theme={null}
import os, time, httpx


class HevnError(Exception):
    def __init__(self, status: int, body: dict):
        error = (body or {}).get("error", {})
        self.status, self.code = status, error.get("code", "internal_error")
        self.details = error.get("details") or {}
        super().__init__(f"{status} {self.code}: {error.get('message', '')}")


class Hevn:
    def __init__(self, session, client_id=None, http=None):
        self.session, self.client_id = session, client_id
        self.http = http or httpx.Client(base_url=os.environ["HEVN_API"], timeout=30)

    def acting_as(self, client_id):
        """A view of the same session that sends X-Hevn-Account on every call."""
        return Hevn(self.session, client_id, self.http)

    def get(self, path, params=None):
        return self.call("GET", path, params=params)

    def post(self, path, body=None, *, idempotency_key=None):
        return self.call("POST", path, body=body, idempotency_key=idempotency_key)

    def put(self, path, body=None, *, idempotency_key=None):
        return self.call("PUT", path, body=body, idempotency_key=idempotency_key)

    def patch(self, path, body=None):
        return self.call("PATCH", path, body=body)

    def delete(self, path):
        return self.call("DELETE", path)

    def call(self, method, path, *, body=None, params=None, idempotency_key=None, retried=False):
        headers = {"Authorization": f"Bearer {self.session.token()}"}
        if self.client_id:
            headers["X-Hevn-Account"] = self.client_id
        if idempotency_key:
            headers["Idempotency-Key"] = idempotency_key
        again = dict(body=body, params=params, idempotency_key=idempotency_key)
        response = self.http.request(method, path, json=body, params=params, headers=headers)
        if response.status_code == 429:
            time.sleep(float(response.headers.get("Retry-After", "1")))
            return self.call(method, path, retried=retried, **again)
        if response.status_code == 401 and not retried:
            self.session.token(refresh=True)
            return self.call(method, path, retried=True, **again)
        payload = response.json() if response.content else {}
        if response.status_code >= 400:
            raise HevnError(response.status_code, payload)
        return payload
```

```javascript Node theme={null}
export class HevnError extends Error {
  constructor(status, body) {
    const error = body?.error ?? {};
    super(`${status} ${error.code ?? "internal_error"}: ${error.message ?? ""}`);
    this.status = status;
    this.code = error.code ?? "internal_error";
    this.details = error.details ?? {};
  }
}

export class Hevn {
  constructor(session, clientId) {
    this.session = session;
    this.clientId = clientId;
    this.api = process.env.HEVN_API;
  }

  actingAs(clientId) {
    return new Hevn(this.session, clientId);
  }

  get(path, params) {
    return this.call("GET", path, { params });
  }

  post(path, body, opts) {
    return this.call("POST", path, { body, ...opts });
  }

  put(path, body, opts) {
    return this.call("PUT", path, { body, ...opts });
  }

  patch(path, body) {
    return this.call("PATCH", path, { body });
  }

  delete(path) {
    return this.call("DELETE", path);
  }

  async call(method, path, { body, params, idempotencyKey, retried = false } = {}) {
    const url = new URL(this.api + path);
    for (const [k, v] of Object.entries(params ?? {})) url.searchParams.set(k, String(v));
    const headers = { authorization: `Bearer ${await this.session.token()}` };
    if (body) headers["content-type"] = "application/json";
    if (this.clientId) headers["x-hevn-account"] = this.clientId;
    if (idempotencyKey) headers["idempotency-key"] = idempotencyKey;

    const response = await fetch(url, { method, headers, body: body && JSON.stringify(body) });
    const again = { body, params, idempotencyKey, retried };
    if (response.status === 429) {
      const wait = Number(response.headers.get("retry-after") ?? 1) * 1000;
      await new Promise((done) => setTimeout(done, wait));
      return this.call(method, path, again);
    }
    if (response.status === 401 && !retried) {
      await this.session.token(true);
      return this.call(method, path, { ...again, retried: true });
    }
    const text = await response.text();
    const payload = text ? JSON.parse(text) : {};
    if (!response.ok) throw new HevnError(response.status, payload);
    return payload;
  }
}
```

```go Go theme={null}
type APIError struct {
	Status  int
	Code    string
	Message string
	Details map[string]any
}

func (e *APIError) Error() string { return fmt.Sprintf("%d %s: %s", e.Status, e.Code, e.Message) }

// Body is a request body, Query a query string, Payload a decoded response.
// Payload.Str walks a dotted path: payout.Str("approval.payload").
type (
	Body    map[string]any
	Query   map[string]string
	Payload map[string]any
)

// ActingAs returns a view of the same session that sends X-Hevn-Account.
func (c *Client) ActingAs(clientID string) *Client {
	bound := *c
	bound.ClientID = clientID
	return &bound
}

func (c *Client) Get(path string, query ...Query) (Payload, error) {
	return c.Call("GET", path, nil, Opts{Query: first(query)})
}

func (c *Client) Post(path string, body Body, opts ...Opt) (Payload, error) {
	return c.Call("POST", path, body, options(opts))
}

func (c *Client) Put(path string, body Body, opts ...Opt) (Payload, error) {
	return c.Call("PUT", path, body, options(opts))
}

func (c *Client) Patch(path string, body Body, opts ...Opt) (Payload, error) {
	return c.Call("PATCH", path, body, options(opts))
}

func (c *Client) Delete(path string, opts ...Opt) (Payload, error) {
	return c.Call("DELETE", path, nil, options(opts))
}

func (c *Client) Call(method, path string, body Body, o Opts) (Payload, error) {
	for attempt := 0; ; attempt++ {
		res, err := c.HTTP.Do(c.request(method, path, body, o))
		if err != nil {
			return nil, err
		}
		payload, status := decode(res)
		switch {
		case status == 429 && attempt < 3:
			time.Sleep(retryAfter(res))
		case status == 401 && attempt == 0:
			if _, err := c.Session.Token(true); err != nil {
				return nil, err
			}
		case status >= 400:
			return nil, asAPIError(status, payload)
		default:
			return payload, nil
		}
	}
}
```

## Download an export

`GET /dapi/v1/transactions/export` streams a file. `download` sends the same headers, raises the same typed error and writes the bytes to disk.

```python Python theme={null}
def download(self, path, params, into):
    """A method on Hevn. The export route streams a file, and the other routes return JSON."""
    headers = {"Authorization": f"Bearer {self.session.token()}"}
    if self.client_id:
        headers["X-Hevn-Account"] = self.client_id
    with self.http.stream("GET", path, params=params, headers=headers) as response:
        if response.status_code >= 400:
            response.read()
            raise HevnError(response.status_code, response.json())
        with open(into, "wb") as file:
            for chunk in response.iter_bytes():
                file.write(chunk)
    return into
```

```javascript Node theme={null}
// A method on Hevn. The export route streams a file, and the other routes return JSON.
export async function download(path, params, into) {
  const url = new URL(this.api + path);
  for (const [k, v] of Object.entries(params ?? {})) url.searchParams.set(k, String(v));
  const headers = { authorization: `Bearer ${await this.session.token()}` };
  if (this.clientId) headers["x-hevn-account"] = this.clientId;

  const response = await fetch(url, { headers });
  if (!response.ok) throw new HevnError(response.status, await response.json());
  await writeFile(into, Buffer.from(await response.arrayBuffer()));
  return into;
}
```

```go Go theme={null}
// Download streams an export to a file. The other routes return JSON.
func (c *Client) Download(path string, query Query, into string) (string, error) {
	res, err := c.HTTP.Do(c.request("GET", path, nil, Opts{Query: query}))
	if err != nil {
		return "", err
	}
	defer res.Body.Close()
	if res.StatusCode >= 400 {
		payload, status := decode(res)
		return "", asAPIError(status, payload)
	}
	file, err := os.Create(into)
	if err != nil {
		return "", err
	}
	defer file.Close()
	_, err = io.Copy(file, res.Body)
	return into, err
}
```

## Confirm until it settles

`confirm_until_settled` follows [Retrying a confirm](https://docs.gethevn.com/developers/signing.md#retrying-a-confirm). On `funding_attempt_expired` it raises `ReopenRequired` (Go returns `ErrReopenRequired`). Re-open with the same `Idempotency-Key`.

```python Python theme={null}
CONFIRM_AGAIN = {"funding_in_progress", "bundler_rejected", "bundler_unavailable"}


class ReopenRequired(Exception):
    """The approval expired. Re-open the payment with the same Idempotency-Key."""


def confirm_until_settled(self, confirm_path: str, signature: str, *, deadline=180.0):
    """A method on Hevn. confirm_path is "/payouts/po_…/confirm" or an escrow action's confirm."""
    started, delay = time.monotonic(), 1.0
    while True:
        try:
            receipt = self.post(confirm_path, {"signature": signature})
            if receipt["status"] != "submitted":
                return receipt
            reason = "submitted"
        except HevnError as error:
            if error.code == "already_funded":
                return {"status": "settled", "transactionHash": error.details.get("transactionHash")}
            if error.code == "funding_attempt_expired":
                raise ReopenRequired(confirm_path) from error
            if error.code not in CONFIRM_AGAIN:
                raise
            reason = error.code
        if time.monotonic() - started > deadline:
            raise TimeoutError(f"{confirm_path} still {reason} after {deadline:.0f}s")
        time.sleep(delay)
        delay = min(delay * 2, 8.0)
```

```javascript Node theme={null}
const CONFIRM_AGAIN = new Set(["funding_in_progress", "bundler_rejected", "bundler_unavailable"]);

export class ReopenRequired extends Error {}

// A method on Hevn. confirmPath is "/payouts/po_…/confirm" or an escrow action's confirm.
export async function confirmUntilSettled(confirmPath, signature, { deadline = 180_000 } = {}) {
  const until = Date.now() + deadline;
  let delay = 1_000;
  for (;;) {
    let reason = "submitted";
    try {
      const receipt = await this.post(confirmPath, { signature });
      if (receipt.status !== "submitted") return receipt;
    } catch (error) {
      if (error.code === "already_funded") {
        return { status: "settled", transactionHash: error.details.transactionHash };
      }
      if (error.code === "funding_attempt_expired") throw new ReopenRequired(confirmPath);
      if (!CONFIRM_AGAIN.has(error.code)) throw error;
      reason = error.code;
    }
    if (Date.now() > until) throw new Error(`${confirmPath} still ${reason}`);
    await new Promise((wake) => setTimeout(wake, delay));
    delay = Math.min(delay * 2, 8_000);
  }
}
```

```go Go theme={null}
var confirmAgain = map[string]bool{
	"funding_in_progress": true, "bundler_rejected": true, "bundler_unavailable": true,
}

var ErrReopenRequired = errors.New("hevn: approval expired, re-open with the same Idempotency-Key")

// confirmPath is "/payouts/po_…/confirm" or an escrow action's confirm.
func (c *Client) ConfirmUntilSettled(confirmPath, signature string) (Payload, error) {
	until, delay := time.Now().Add(3*time.Minute), time.Second
	for {
		receipt, err := c.Post(confirmPath, Body{"signature": signature})
		var apiErr *APIError
		switch {
		case err == nil && receipt.Str("status") != "submitted":
			return receipt, nil
		case err == nil: // submitted: no receipt yet, confirm again
		case !errors.As(err, &apiErr):
			return nil, err
		case apiErr.Code == "already_funded":
			return Payload{"status": "settled", "transactionHash": apiErr.Details["transactionHash"]}, nil
		case apiErr.Code == "funding_attempt_expired":
			return nil, ErrReopenRequired
		case !confirmAgain[apiErr.Code]:
			return nil, err
		}
		if time.Now().After(until) {
			return nil, fmt.Errorf("hevn: %s did not settle in time", confirmPath)
		}
		time.Sleep(delay)
		if delay < 8*time.Second {
			delay *= 2
		}
	}
}
```

## Poll a read

Use `poll_until` for client provisioning, KYB review, virtual account activation and payin settlement. The delay backs off from 1 to 8 seconds with jitter.

```python Python theme={null}
import random, time


def poll_until(read, done, *, deadline=120.0, first=1.0, cap=8.0):
    """Call read() until done(value) is true. Returns the last value or raises TimeoutError."""
    started, delay = time.monotonic(), first
    while True:
        value = read()
        if done(value):
            return value
        if time.monotonic() - started > deadline:
            raise TimeoutError(f"still {value.get('status')} after {deadline:.0f}s")
        time.sleep(delay + random.uniform(0, delay / 2))
        delay = min(delay * 2, cap)
```

```javascript Node theme={null}
export async function pollUntil(read, done, { deadline = 120_000, first = 1_000, cap = 8_000 } = {}) {
  const until = Date.now() + deadline;
  let delay = first;
  for (;;) {
    const value = await read();
    if (done(value)) return value;
    if (Date.now() > until) throw new Error(`still ${value.status} after ${deadline}ms`);
    await new Promise((wake) => setTimeout(wake, delay + Math.random() * (delay / 2)));
    delay = Math.min(delay * 2, cap);
  }
}
```

```go Go theme={null}
func PollUntil[T any](read func() (T, error), done func(T) bool, deadline time.Duration) (T, error) {
	until, delay := time.Now().Add(deadline), time.Second
	for {
		value, err := read()
		if err != nil {
			return value, err
		}
		if done(value) {
			return value, nil
		}
		if time.Now().After(until) {
			return value, fmt.Errorf("hevn: still pending after %s", deadline)
		}
		time.Sleep(delay + time.Duration(rand.Int63n(int64(delay/2))))
		if delay < 8*time.Second {
			delay *= 2
		}
	}
}
```

## Go helpers

```go Go theme={null}
type Client struct {
	API      string
	ClientID string
	HTTP     *http.Client
	Session  *Session
}

type Opts struct {
	Query          Query
	IdempotencyKey string
}

type Opt func(*Opts)

func IdempotencyKey(key string) Opt { return func(o *Opts) { o.IdempotencyKey = key } }

func options(opts []Opt) Opts {
	var o Opts
	for _, apply := range opts {
		apply(&o)
	}
	return o
}

func first(queries []Query) Query {
	if len(queries) == 0 {
		return nil
	}
	return queries[0]
}

// at walks a dotted path and returns nil when any step is missing.
func (p Payload) at(path string) any {
	var node any = map[string]any(p)
	for _, step := range strings.Split(path, ".") {
		object, ok := node.(map[string]any)
		if !ok {
			return nil
		}
		node = object[step]
	}
	return node
}

func (p Payload) Str(path string) string { value, _ := p.at(path).(string); return value }
func (p Payload) Bool(path string) bool  { value, _ := p.at(path).(bool); return value }

// List walks a dotted path to an array of objects: catalogue.List("rails").
func (p Payload) List(path string) []Payload {
	items, _ := p.at(path).([]any)
	page := make([]Payload, 0, len(items))
	for _, item := range items {
		if object, ok := item.(map[string]any); ok {
			page = append(page, object)
		}
	}
	return page
}

func (c *Client) request(method, path string, body Body, o Opts) *http.Request {
	var reader io.Reader
	if body != nil {
		encoded, _ := json.Marshal(body)
		reader = bytes.NewReader(encoded)
	}
	request, _ := http.NewRequest(method, c.API+path, reader)
	token, _ := c.Session.Token(false)
	request.Header.Set("Authorization", "Bearer "+token)
	if body != nil {
		request.Header.Set("Content-Type", "application/json")
	}
	if c.ClientID != "" {
		request.Header.Set("X-Hevn-Account", c.ClientID)
	}
	if o.IdempotencyKey != "" {
		request.Header.Set("Idempotency-Key", o.IdempotencyKey)
	}
	if len(o.Query) > 0 {
		query := request.URL.Query()
		for name, value := range o.Query {
			query.Set(name, value)
		}
		request.URL.RawQuery = query.Encode()
	}
	return request
}

func decode(res *http.Response) (Payload, int) {
	defer res.Body.Close()
	var payload Payload
	_ = json.NewDecoder(res.Body).Decode(&payload)
	return payload, res.StatusCode
}

func retryAfter(res *http.Response) time.Duration {
	seconds, err := strconv.Atoi(res.Header.Get("Retry-After"))
	if err != nil || seconds <= 0 {
		return time.Second
	}
	return time.Duration(seconds) * time.Second
}

func asAPIError(status int, payload Payload) *APIError {
	body, _ := payload["error"].(map[string]any)
	details, _ := body["details"].(map[string]any)
	code, _ := body["code"].(string)
	message, _ := body["message"].(string)
	if code == "" {
		code = "internal_error"
	}
	return &APIError{Status: status, Code: code, Message: message, Details: details}
}
```

## Assemble the client

```python Python theme={null}
key = Key()
hevn = Hevn(Session(key))                      # unbound: /clients and /escrow
client = hevn.acting_as("cl_7YQ2Kf3mN8")    # bound to one client
me = hevn.acting_as(os.environ["HEVN_USER_ID"])  # bound to your account, for /client*
```

```javascript Node theme={null}
const key = new Key();
const hevn = new Hevn(new Session(key));                 // unbound: /clients and /escrow
const client = hevn.actingAs("cl_7YQ2Kf3mN8");       // bound to one client
const me = hevn.actingAs(process.env.HEVN_USER_ID);     // bound to your account
```

```go Go theme={null}
key, err := hevn.NewKey()
api := &hevn.Client{API: os.Getenv("HEVN_API"), HTTP: http.DefaultClient, Session: &hevn.Session{Key: key}}
client := api.ActingAs("cl_7YQ2Kf3mN8")
me := api.ActingAs(os.Getenv("HEVN_USER_ID"))
```


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