Skip to main content
Seven rules hold for all 45 operations. Read them once and no endpoint will surprise you.

Money

Every amount on the wire is a decimal string in the asset’s major unit: "500.00", "0.9247", "1204.500000". Not a JSON number, not an atomic-unit integer, never exponent notation. Precision is refused, never rounded. More fractional digits than the asset allows answers 422 validation_failed with details.fields[0].code = "amount_precision" — so a rounding error can never reach a payment. Rates and fees follow the same rule; feeBps is the one exception and is an integer number of basis points. Where an operation has two sides, you pin one and HEVN derives the other. Send amount to pin what leaves the balance or amountTo to pin what the beneficiary receives — one of them, never both.
A decimal arrives in JavaScript and Go as a string. A typeof x === "number" guard silently skips every amount this API returns; parse with a decimal type, not a float.

Ids

Every id is prefixed, opaque and safe to store as text: A dk_… is the one id you never send to this API: it names a developer key registration and belongs to the management routes on the ordinary API (Developer key). Do not parse them, derive them or assume a length. An id of the wrong kind in a body or query field is 400 invalid_id with the expected prefix in details; in a path it is that resource’s own 404, so an id from another namespace is never confirmable. Rail ids — sepa_named-bank_a, a stand-in for a real one — are the exception that proves the rule: they are opaque strings you copy from GET /dapi/v1/banks, they carry no prefix, and their shape is not a contract. See Rails.

Casing

camelCase everywhere: request bodies, query parameters, response fields and the keys inside error.details. Two things are deliberately not field names. An error.code slug is always lowercase snake_case. An enum member usually is too — business_revenue — but an enum member is a literal, not a naming convention: HighRiskActivity members are SCREAMING_SNAKE_CASE (MONEY_SERVICES, PRECIOUS_METALS_STONES_JEWELRY), and FiatTransactionCount members are the ranges they read as (<10, 10-50, 50-200, 200-1,000, 1,000+, punctuation included). Send and compare enum values exactly as the spec prints them, and never case-fold them. Two consequences worth knowing before your first 422:
  • Unknown keys are refused. A body with a field this version does not define answers 422 validation_failed with code: "extra_forbidden" rather than ignoring it, so a typo cannot silently drop a payment reference. The two nested objects that predate this rule — a contact’s address and a requisite’s bank/payer block — still ignore a key they do not know, so a misspelt streetAddress there resurfaces one step later as 422 contact_details_invalid naming the field it never received.
  • Null fields are omitted. “Absent” and “null” are the same thing; a response never prints "phone": null. Treat a missing key as unset.
One corner is not camelCase and cannot be: the keys inside fields on PUT /dapi/v1/documents/{documentId}/content are the extractor’s own field names — registration_number, address.street_address — because they are values read off a page rather than fields of this API. They are the keys the refusal that asks for them already named. See Onboard a client. Timestamps are RFC 3339 in UTC with a Z suffix: 2026-09-17T10:04:11Z.

Idempotency-Key

Every write accepts Idempotency-Key, matching ^[A-Za-z0-9._:-]{1,128}$. No operation requires it. Send one whenever a retry must not become a second payment. Derive it from an id your own system owns, and keep it identical across every retry of the same request. Derive Idempotency-Key from an id your own system already owns, pass it in from the caller, and keep it identical across every retry of the same payment.
Never generate the key inside the helper that sends the request, and never rotate it on a retry — both turn one payment into two. The format and the replay windows are in Conventions. When you omit it, HEVN derives a fingerprint from the request itself, answers with it as idempotencyKey, and treats an identical repeat as a replay. The difference is the window: a key you chose anchors the request for 7 days, a derived one for 2 hours — long enough for a retry, short enough that two genuinely separate payments of the same amount both happen. A key is scoped to your account and the operation, so two integrators can never collide and the same key on POST /payouts and POST /contacts is not a replay. Reusing one with a different body is 409 idempotency_key_reused, and details names the resource the key already belongs to. A malformed one is 400 idempotency_key_invalid.

Pagination

Every list is a cursor page:
Pass limit (1 to 100, default 50) and cursor. Pass back the nextCursor you were given, verbatim and unparsed; when it is absent you have reached the end. There is no offset and no total count anywhere, and a cursor that does not belong to the query it is sent to is 400 invalid_cursor. Aggregates are their own route rather than a field on page one — GET /dapi/v1/transactions/summary takes the same filters as GET /dapi/v1/transactions.

Status codes

There is no 204: every response has a body. Location and pollUrl are paths from the host root and already include the /dapi/v1 prefix. One of them names no resource: a client’s pollUrl is literally /dapi/v1/client, so poll it with the cl_… you were just given in X-Hevn-Account — the URL alone identifies nothing.

Errors

One envelope, for every refusal, on every route:
code is a stable lowercase slug — branch on it. message is written for a human reading a log and may be reworded at any time. details is optional, camelCased, and carries the fields that make the refusal actionable: blockers on a rail, fields on a validation failure, transactionHash on a payment that already went through, requestId on a 5xx — quote that one to support.
details is diagnostic, not part of the money contract: the funding refusals report available and required in atomic units ("3000000" is 3 USDC), which is the one place an amount is not a major-unit decimal string. Read them for a log line, not for arithmetic.
Field-level failures collapse into one shape, so you can render them next to your form inputs:
The whole slug registry, grouped by resource, is in Errors; the numbers behind every limit are in Limits.

Next: Signing

Prepare, check, sign, confirm — the one mechanism behind every payment.