XKOVA Docs

Embedded Member Integration

Some XKOVA flows run in a member's browser, not in your backend. A recipient claims value from an escrow, a sender signs a payment in their own wallet, a member signs in and manages their profile. These surfaces are built to be embedded directly in your web app, and each one carries its own credential so you never have to ship a tenant API key to the browser. This page covers the four member surfaces you embed, the tokens that authenticate them, and the origin and redirect allowlists that keep them safe.

What You Embed

Your backend holds the tenant API key and does the server-to-server work: creating payments, provisioning members, minting claim links. The browser handles the steps a person does in front of a screen. Those member surfaces authenticate with a scoped capability token, not your API key, so they are safe to call from client code.

SurfaceCredentialRuns in
Claim ceremonyThe claim token in the URL pathBrowser, directly against the API
Signing sessionThe HMAC signing token in the query stringBrowser, directly against the API
Member sessionTenant API key to obtain, then a member access tokenYour backend obtains it; the browser holds the returned token set
Avatar and receive QRTenant API keyYour backend; the browser does the direct-to-storage upload
The rule of thumb: a token that identifies one member or one pending transfer is safe in the browser; the tenant API key, which can act for the whole tenant, is not. Keep the API key server-side and exchange it for a member token before any client code runs.

The Claim Ceremony

When a payment is sent to someone who is not yet a member, it lands in an escrow and XKOVA issues a claim link. The recipient opens that link in a browser and proves they control the contact on file before value is released. Every call is authenticated by the claim token in the path, so your front end drives the whole flow with no API key. See Escrow and Claims for how the escrow is created on the sender side.

  1. Fetch context. GET /v1/claims/{token} returns the claim state (pending, claimed, cancelled, or expired), the amount, token_symbol and token_decimals, the recipient_ref the code is delivered to, the token and claim expiries, an otp_verified flag, resolved branding, and the available_paths the recipient may choose. Render the landing page from this.
  2. Send a code. POST /v1/claims/{token}/send-otp with { channel, identifier }, where channel is email or sms. The response carries a challenge_id and an expires_at.
  3. Verify the code. POST /v1/claims/{token}/verify-otp with { challenge_id, code }, where code is the six digits the recipient received. On success you get { verified: true, resolve_grant }. The resolve_grant is a one-time secret proving this caller passed the recipient-bound challenge; thread it into the next step.
  4. Resolve. POST /v1/claims/{token}/resolve with { path, payload, resolve_grant }. The path is external_wallet (release now to payload.address) or wait (leave the escrow open until expiry). The resolve_grant is required, so a token holder who never verified the code cannot release the escrow.
flowchart TD
  A[Open claim link in browser] --> B[GET claim context]
  B --> C[Send OTP to email or sms]
  C --> D[Verify OTP code]
  D --> E{resolve_grant issued}
  E --> F[Resolve: external_wallet]
  E --> G[Resolve: wait until expiry]
  F --> H[Value released to supplied wallet]
  G --> I[Escrow stays open]

Two recovery calls handle a stale link. POST /v1/claims/{token}/refresh mints a fresh token for the same escrow and returns the new claim_url. POST /v1/claims/{token}/resend mails a fresh link to the recipient on file and always returns the same 202 body with no escrow detail, so a holder of a leaked or expired token cannot fingerprint the escrow from the response.

The Member Session

The wrapped member-auth surface at /v1/member-auth/* gives your members email-and-password, magic-link, and password-reset sign-in, plus TOTP MFA. Every endpoint requires a tenant-scoped API key, so your backend calls this surface, then hands the returned token set to the browser. The member's session token is what your client code carries afterward. The identity provider stays an internal detail behind this wrapper; your integration only ever talks to XKOVA.

StepEndpointReturns
RegisterPOST /v1/member-auth/sign-upA token set, or a pending-verification response when email confirm is required
Confirm emailPOST /v1/member-auth/email/confirmA token set, or an MFA challenge
Sign inPOST /v1/member-auth/sign-inA token set, or an mfa_required challenge
Complete MFAPOST /v1/member-auth/sign-in/mfa/verifyThe token set the first leg withheld
Magic linkPOST /v1/member-auth/sign-in/magic-link/request then .../confirmA sent acknowledgement, then a token set
RefreshPOST /v1/member-auth/refreshA rotated token set
Read sessionGET /v1/member-auth/sessionThe member profile for the current token
Sign outPOST /v1/member-auth/sign-out204

When a member has a verified MFA factor, sign-in returns no token at all. Branch on the response status field: a value of mfa_required carries an mfa_token, an expires_in, and a factors array, each entry a { factor_id, type, friendly_name }. You pass the mfa_token and the chosen factor's factor_id with the member's code to POST /v1/member-auth/sign-in/mfa/verify to obtain the session. Enrollment and management run at POST /v1/member-auth/mfa/enroll, POST /v1/member-auth/mfa/{factorId}/verify, GET /v1/member-auth/mfa, and DELETE /v1/member-auth/mfa/{factorId}. The SDK exposes this surface as a wrapped member-auth client (signIn, verifySignInMfa, refresh, session, and the rest) so you do not hand-roll the token branching; see the SDK overview.

The email flows carry a redirect URL your app receives the member back on. XKOVA only sends a member to a URL your tenant has allowlisted first, covered under Browser Security Posture below. An unallowlisted URL is rejected with member_auth_redirect_url_not_allowlisted.

Signing Sessions and the Wallet Handshake

A payment or escrow that needs a member's signature lands in a pending-sign state and XKOVA returns a signing_url. That URL carries a short-lived HMAC token in its query string, and it is the credential for the three signing endpoints, so the member signs directly from the browser with no API key. This signature is what makes the transfer non-custodial: the member signs, a relayer submits, and the contract verifies on chain before it moves anything. The full mechanics are in the Non-custodial Model.

  1. Load the session. GET /v1/payments/{id}/signing-session?token=... returns the resolved branding, a narrow payment_preview (addresses, symbol, gross and net amounts, the fee snapshot, memo, and chain), and two EIP-712 typed-data documents: typed_data for the transfer intent and permit_typed_data for the gasless token approval. Render the preview so the member sees exactly what they are signing.
  2. Get the wallet token. GET /v1/payments/{id}/member-jwt?token=... mints a short-lived JWT and returns { jwt, expires_at, external_user_id }. The member's in-app wallet connects with this JWT; because the wallet address is derived deterministically from the token's subject, the member gets the same address every time.
  3. Sign and submit. The wallet signs both typed-data documents, then you POST /v1/payments/{id}/signatures with the signatures and the same signing token in the body. XKOVA recovers the signer, builds the meta-transaction calldata, and the XKOVA Relayer submits it on chain and pays the gas.
The typed-data wire format encodes every uint256 as a decimal string because JSON cannot carry a bigint. Restore each of those fields to a bigint before your wallet signs, or the recovered signer will not match and the submit call returns authorization_invalid. A direct send needs both signatures (the transfer intent and the permit); an escrow signature needs one.

Escrows follow the same three-endpoint shape under /v1/escrows/{id}/..., with an intent_type of create or cancel selecting which action the member is signing. The signing session is issued only after the member's identity check clears; a member who is not yet verified gets verification_required, and a payment with an open compliance case gets compliance_case_open. See Identity Verification and Payments.

Avatars and Receive QR

A member avatar uploads in three steps so the image bytes go straight to storage and never transit the API. Your backend mints the upload URL with the tenant API key; the browser does the direct upload.

  1. POST /v1/account-holders/{id}/avatar/upload-url with the content type and length returns { upload_url, object_key, expires_at }.
  2. The browser sends a PUT to upload_url with the matching Content-Type and Content-Length headers. The bytes go to storage, not to XKOVA.
  3. POST /v1/account-holders/{id}/avatar/confirm with { object_key } persists the avatar on the member and returns the updated account holder.

Read the current avatar with GET /v1/account-holders/{id}/avatar, which returns { url } as a short-lived presigned URL or null, and remove it with DELETE /v1/account-holders/{id}/avatar. The resolved URL is safe to embed in your member app. See Account Holders for the member record these attach to.

To let a member receive value by scan, POST /v1/wallets/{id}/qr generates a receive QR for a wallet under the calling tenant. The body optionally carries an amount, a memo, and a payment_request_id; the response is { uri, png_data_url }, where png_data_url is a data URL you can render inline. See Wallets and Balances.

Browser Security Posture

Two tenant-managed allowlists govern where member-auth flows may run and land. Manage them from your backend, then your embedded flows work from the origins you approved.

ControlEndpointEnforced by
Allowed browser originsGET / POST /v1/tenant/member-auth/allowed-origins, DELETE .../{id}Member-auth requests from an origin not on the list are rejected with member_auth_origin_not_allowlisted
Allowed redirect URLsGET / POST /v1/tenant/member-auth/redirect-urls, DELETE .../{id}Email flows only redirect to an allowlisted URL; others are rejected with member_auth_redirect_url_not_allowlisted

An allowed origin is an exact scheme://host[:port] match. A redirect URL is registered against a purpose of email_confirm, password_reset, or magic_link, and must match the value passed in the corresponding member-auth call. Register both before you ship the member app, and add your local development origin during build-out; see Local Development.

The credential rules follow from the surface. The tenant API key never belongs in client code: obtain member tokens, signing tokens, and claim tokens server-side or from XKOVA, and let only those reach the browser. Treat every capability token as a secret with a short life: claim tokens and signing tokens expire, member access tokens rotate through refresh, and a leaked one grants only the single member or transfer it names. Your tenant API key, by contrast, acts for the whole tenant, so its exposure is a different order of problem. See Authentication for the full credential model.

Errors Worth Handling

The error catalog documents every code; these are the ones the member surfaces most commonly return.

SurfaceCodes
Claim ceremonynot_found, invalid_state (already claimed or expired), validation_failed
Member sessionmember_auth_invalid_credentials, member_auth_email_not_confirmed, member_auth_mfa_required, member_auth_mfa_code_invalid, member_auth_rate_limited, member_auth_redirect_url_not_allowlisted, member_auth_origin_not_allowlisted
Signing sessionunauthorized (an expired or invalid signing token), verification_required, compliance_case_open, not_found
Submit signaturesinvalid_state (already signed or past signing), authorization_invalid (the signature does not recover to the expected signer), service_unavailable

Related

For the exact request and response shapes, see the operations list or try the calls live in the interactive reference.