XKOVA Docs

Account Holders

An account holder is the member record your integration acts on behalf of: the person or business that holds a wallet, signs transfers, and appears on the cap table. Every member-facing capability hangs off this record, so it is worth knowing what it carries beyond the wallet.

Provisioning: explicit or just in time

Your backend creates account holders in one of two shapes. Explicit creation registers the member ahead of time. Just-in-time provisioning creates the record on the member's first sign in, which is the natural shape when your members already exist in your own identity system and you only want an XKOVA record once they first touch the rails. Both paths produce the same record; the JIT call is idempotent for the same member, so a race between two first requests resolves to one holder.

Verification (KYC for a person, KYB for a business) attaches to the account holder, and the verification gate reads the holder's effective status on every gated action. See Identity Verification.

The self-service surface

A member, authenticated through any of the member identity patterns, can read and act on their own record:

  • Balances. The member's stablecoin balance, maintained from on-chain activity and continuously reconciled. Where the tenant runs internal bank sub-accounts (checking and savings), the member can read those balances and their transaction history, and transfer between their own sub-accounts.
  • Contacts. A personal address book of reused recipients the member can create, update, delete, and search, so a repeat send does not re-enter the destination.
  • Consents. The tenant's policy consent state: what the member has accepted, what is pending, and an accept call your app drives at sign-in or first use. Consent acceptance is recorded append-only, so the trail of who accepted what, and when, is preserved.
  • Data export. A right-to-access export the member can request and poll until the bundle is ready.
  • Avatar. An optional profile image, uploaded through a presign-then-confirm flow (direct-to-storage upload, then a confirm call that persists it) and read back through a short-lived signed URL rather than a permanent public link. Deleting it removes the stored image outright.

Contacts

Contacts are a per-member address book of reused recipients, reached under /v1/contacts. The surface is tenant scoped: your backend authenticates with a tenant-scope API key and names the owning member with an account_holder_id on every call, because it already knows who is signed in from your own auth flow. A partner-scope key cannot reach this surface and is rejected with partner_scope_denied.

OperationMethod and pathPermission
List a member's contactsGET /v1/contactscontact.read
Read one contactGET /v1/contacts/{id}contact.read
Create a contactPOST /v1/contactscontact.write
Update a contactPATCH /v1/contacts/{id}contact.write
Delete a contactDELETE /v1/contacts/{id}contact.write
Look up by email or phonePOST /v1/contacts/searchcontact.read

Create takes a required display_name plus optional email, phone, wallet_address, and notes, and returns 201 with the stored contact. Update is a partial PATCH that returns 200, and delete returns 204. List and search are cursor paged and return newest first. Each stored contact carries id, account_holder_id, display_name, wallet_address, notes, last_sent_at, pending_resolution, and its created and updated timestamps.

Email and phone are hashed at the API boundary with a per-tenant pepper before anything reaches storage, and plaintext is never persisted. Reads return the hashes as email_hash and phone_hash, not the original values. Because the hash uses a per-tenant pepper, it is stable within your tenant, so lookups work, and divergent across tenants, so a contact cannot be confirmed across a tenant boundary.

Look up an existing contact with POST /v1/contacts/search rather than a query parameter: the plaintext email or phone travels in the request body, so it never lands in a URL or an access log. The API hashes the supplied value with the same tenant pepper and matches it against the member's stored hashes, with OR semantics across email and phone. Use it for the "do I already have this recipient?" prompt and to dedupe before creating a new row.

A contact can also be created by the send flow against a recipient who has not enrolled yet, in which case wallet_address is still null and pending_resolution is true; both backfill once that recipient claims. Create, update, and delete are audited.

A tenant can turn contacts off with a kill switch. While it is off, create and update return 403 contacts_disabled, while reads and delete still work, so your app can keep rendering a member's existing contacts.

Staff actions: search, hold, release, close

Operators work the same records from the console's Accounts directory (which lists member account holders alongside staff and treasury vaults). A compliance officer can place a hold on an account holder, recording a reason; a held account cannot move value until released. Closing an account holder is the terminal action. All three are audited. See Compliance for the operator journey.

Related

For the account holder, contact, consent, export, and avatar operations, see the operations list or the interactive reference.