# Yutai docs The developer docs of Yutai, the loyalty network for tokenized equity: how a wallet, exchange or broker integrates, the SDK's elements and the distributor API's calls. --- # Quickstart The first walk a distributor takes with Yutai, using only these docs: get a key, install the web SDK, read a holder's benefits, and show the seal the benefits module draws, all on your own page. 1. 1 Get a key Every call to Yutai's distributor API presents one key, and Yutai issues the keys. Ask your operator at Yutai for the key your integration calls with, and keep it where a server keeps it: the key never rides code a browser loads. [The get started page](/docs/get-started) carries the path to a first call: what to send an operator, who approves the request, and how the key reaches you once. [The API reference](/docs/api) names the key a call presents and what one key may call. 2. 2 Install the web SDK Add this one script tag to the page that draws Yutai's elements, once, ahead of any element it writes: The script tag `` The script registers Yutai's elements, and each element's attributes and tokens are documented on its own page: the [credential card page](/docs/credential-card) carries the card's. 3. 3 Read a holder's benefits Paste this call where your page runs its server code, with the placeholder replaced by the key from the first step. The answer carries the wallet's holdings and the benefits each holding reaches. The call to paste `import { connectDistributorApi } from "@yutai/sdk-web/client"; const client = connectDistributorApi({ base: "https://api.yutai.com", key: "ytk_your-key" }); const answer = await client.askEligibility(["0xcd8a4c3f9d3ab5c8817729a4d1420b95869b5928"]);` The call is the exact one the [example wallet page](/docs/examples/wallet) runs and prints, and that page draws the same wallet's answer as a list, so you can see the shape before you wire your own screen. Beneath it, the sample answer the API's own document carries for this read: the document's own example, with its own example wallet, never a live read. The sample answer for GET /v1/eligibility `{ "wallets": [ { "wallet": "0xabc0000000000000000000000000000000000001", "read": true, "sourceMode": "SIMULATED", "tokens": [ { "issuer": null, "security": { "name": "Sample Global Holdings", "isin": "US0000000009" }, "wrapper": "Sample token programme", "chain": "eip155:88880", "shares": "15", "daysHeld": 452, "tier": "Patron", "benefits": [ { "benefitId": "bft_01K4V9QG000000000000000001", "name": "Sample lounge access", "type": "ACCESS", "delivery": "VENUE_REDEMPTION", "state": "eligible", "illustrative": true } ] } ] }, { "wallet": "0xdef0000000000000000000000000000000000002", "read": false, "sourceMode": "SIMULATED", "unreadReason": "Yutai has not read this wallet yet." } ] }` 4. 4 Show the seal Hand the answer to the benefits module: the element draws one row per token, the benefits each holding reaches beneath it, and the claim sheet runs end to end inside the element, so no key reaches the browser. Sample figures: a simulated holder's tokens and benefits, not a real holder's. The element's markup and its attributes are documented on the [benefits module page](/docs/benefits-module). --- # Get started A first call to the distributor API needs one key, and Yutai issues keys through its operators, not through a form. This page walks the path in three steps: what you send, who approves it, and how the key reaches you once. There is no form on this page: you send the ask to Yutai's operators in your first conversation, and the steps below are what happens with it. Nothing here sends anything. ## The ask Send an operator three things. First, the name of your company as Yutai knows it: Yutai keeps every partner in its records as a distributor, and the key is made for the right distributor. Second, the name the key goes by, a short label you will recognise later. Third, the calls the key needs, picked from the closed list at the bottom of this page. An operator of Yutai's network asks for the key with these facts, and the request then waits for an owner. If no owner answers, the request lapses after `3 days`. ## The approval An owner of Yutai approves the request on their own passkey; one owner's approval is enough, and an owner who asked may approve their own request. No key exists before that approval stands, and the approval opens the collect window described next. ## The collect The person who asked collects the key once, within `1 day` of the approval. The key is shown that one time and is stored nowhere: what Yutai keeps is only its first characters, so an operator can tell which key is which, and a proof of the whole key. Only the person who asked can collect it, and if the window closes first the request reads expired, so a new ask starts the path again. ## The key Every call presents the key in the Authorization header, as a Bearer key: `Authorization: Bearer ytk_your-key`. A publishable key starts with ytk_pub_ and carries 64 lowercase hex characters after it. A secret key starts with ytk_ and carries 64 lowercase hex characters after it. The placeholder `ytk_your-key` stands for the key you hold; no real key rides this page. ## The scopes A key carries only the calls it was asked for, each from one closed list. The operator picks them with you when the ask is sent, and each scope's call is described on the API reference page: [holders.resolve](/docs/api): Resolve the holder a wallet belongs to. [credentials.read](/docs/api): Read a holder's credential status. [benefits.list](/docs/api): List a holder's benefits. [eligibility.read](/docs/api): Read the benefits one or more wallets reach. [availability.read](/docs/api): List a security's available benefits. [claims.write](/docs/api): Record a benefit claim. [connect.write](/docs/api): Create a Connect session and unlink a customer account. [connect.read](/docs/api): Read Connect sessions and their outcomes. To make your first call with the key, follow the [Quickstart](/docs/quickstart). When a call is refused, the [Errors and limits](/docs/errors) page says what the refusal means. Once the key is in hand, the formats every value travels in are the [identifiers page](/docs/identifiers)'s, and the [example wallet](/docs/examples/wallet) shows a working call end to end. --- # Identifiers How Yutai writes every identifier a distributor sends or reads: chains, instruments, companies' shares, wallet addresses, quantities, instants and the ids Yutai gives its own records. The values on this page are read from Yutai's own rules when the site is built, so they cannot drift from them. Yutai does not serve the distributor interface yet. These are the formats its records and its interface description already use, so what you build against them now stays the same. ## Chains A chain is written as its CAIP-2 id, `namespace:reference`: `eip155` and the decimal chain id for an EVM chain, `solana` and the first 32 characters of the genesis hash for Solana. The chains below are the live chains the product supports. A chain whose plain name is not confirmed yet is shown by its id with the note (name to confirm), never a guessed name. `eip155:1`: Ethereum mainnet `eip155:10`: eip155:10 (name to confirm) `eip155:56`: BNB Smart Chain mainnet `eip155:196`: eip155:196 (name to confirm) `eip155:999`: eip155:999 (name to confirm) `eip155:4663`: Robinhood Chain mainnet `eip155:5000`: eip155:5000 (name to confirm) `eip155:8453`: Base mainnet `eip155:42161`: Arbitrum One `eip155:57073`: eip155:57073 (name to confirm) `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp`: Solana mainnet ## Instruments An on-chain share is written as its CAIP-19 id: the chain's CAIP-2 id, a slash, then the asset namespace and the contract address, `chain/namespace:address`. The namespace follows the chain family: `erc20` on an EVM chain, `token` on Solana. EVM: `eip155:1/erc20:0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed` Solana: `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token:BPRAnfLMc5VxDMdQsAYK44F3kRamLDsy25q6oT9Rrr8w` ## Companies' shares A company's share is named by its ISIN: twelve characters, two capital letters for the country, nine capital letters or digits, then one check digit. Yutai groups every on-chain share of one company under that ISIN, for example `US0378331005`. ## Wallet addresses A wallet is written the way its chain family writes it. EVM: 0x and 40 hex characters. Mixed case must match the EIP-55 checksum; one case is accepted too. Two EVM addresses compare in lower case. For example `0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed`. Solana: Base58, 32 to 44 characters, decoding to exactly 32 bytes. Solana addresses compare exactly, case included. For example `4Nd1mBQ6MJ6ViN5jM2Z4atPozHvGvQtME5hkmZjpSXRV`. ## Quantities Every quantity travels as a decimal string, never as a JSON number and never as a float, so no digit is lost or rounded. A quantity of shares has no sign, no leading zero before a whole number and no trailing zero after the point. An amount in base units, the whole number a chain answers, is a string too: below, the same 12.5 shares at 18 decimals. Quantities as strings ``` { "quantity": "12.5", "amount": "12500000000000000000" } ``` ## Instants Every instant is an ISO 8601 string in UTC, for example `2026-10-08T09:30:00Z`. It is never a number of seconds. ## Prefixed ids Yutai's own records carry a prefix that says what they are, then a 26-character ULID. Treat the whole id as opaque text. `apk_`: An API key's record. It names the key so you can tell keys apart; it is not the key, which is shown to you once when it is issued. For example `apk_01J9ZQ4V8W7X6Y5Z4A3B2C1D0E`. `whs_`: A webhook subscription: the one address your webhooks go to and the kinds of change you asked for. For example `whs_01J9ZQ5B7C8D9E0F1G2H3J4K5M`. `whd_`: One signed webhook delivery of one change to your address. A retry keeps the same id, so you can tell a repeat from a new change. For example `whd_01J9ZQ6N8P9Q0R1S2T3V4W5X6Y`. ## Where these are used [The API reference](/docs/api) lists the calls that take these formats. [Errors and limits](/docs/errors) says what a refused value answers. [The Example wallet](/docs/examples/wallet) runs a whole read with values in these formats. --- # Overview The three ways a distributor connects to Yutai, and the kinds of distributor it serves, in one page. If this is your first integration, the [Get started](/docs/get-started) page walks the path to a first call: what to send, who approves it, and how the key reaches you once. Every state below is read when the site is built, from the zones the preview serves, the elements the SDK registers and the routes the site's API answers, so what you read is what Yutai serves today. Every state on this page is read when the site is built, never retyped by hand: when Yutai ships what a row reading Planned names, the page reads it the same day. ## Ways to integrate A distributor connects through one way or several: the hosted app, the embedded elements, or the distributor API. Each way carries its own state beside it. ## The hosted holder app Available Yutai serves the holder app itself: the holder opens it, sees their own credential, tier and benefits, and your site hosts no line of it. [Open Yutai app](/app) ## The SDK's elements Available The SDK's elements render Yutai's own components inside your pages, styled with the tokens you set. The elements registered today are in the SDK; each names the docs page that explains it when one stands, and the docs still planned for the rest stand below. `yutai-credential-card`: Available [Credential card](/docs/credential-card) `yutai-tier-badge`: Available [Tier badge](/docs/tier-badge) `yutai-benefits-list`: Available [Benefits list](/docs/benefits-list) `yutai-benefits-module`: Available [Benefits module](/docs/benefits-module) ## The distributor API Callable today Your server asks the distributor API for the facts behind your screens, with a key the platform issues. Each call reads the state its route answers, and every path links its own call page, where the call's parameters, its answer and its errors stand. [GET /v1/eligibility](/docs/api/getWalletEligibility): Callable today [GET /v1/availability](/docs/api/getBenefitAvailability): Callable today [GET /v1/registry/companies](/docs/api/listRegistryCompanies): Callable today [GET /v1/registry/instruments](/docs/api/readRegistryInstrumentFacts): Callable today [GET /v1/claims/{id}](/docs/api/getClaim): Callable today [POST /v1/claims/{id}/link](/docs/api/reissueClaimLink): Callable today [GET /v1/status](/docs/api/getApiStatus): Callable today [POST /v1/claims/challenge](/docs/api/requestClaimChallenge): Callable today [POST /v1/claims](/docs/api/recordClaim): Callable today [GET /v1/fulfilments](/docs/api/listPendingFulfilments): Callable today [POST /v1/fulfilments/{id}/confirm](/docs/api/confirmFulfilment): Callable today [POST /v1/connect/sessions](/docs/api/createConnectSession): Callable today [GET /v1/connect/sessions/{id}](/docs/api/getConnectSession): Callable today ## The kinds of distributor A distributor is one of three kinds. What Yutai reads, what the distributor sends, how long a holder has held and how a claim is proven differ by the kind. The three kinds, side by side: | What differs | Self-custody wallet | Custodial exchange or broker | Other platform | | --- | --- | --- | --- | | What Yutai reads | The chains the distributor registered: Yutai reads the holder's wallets on them straight from the chain. | The registry instruments the distributor listed: the distributor signs what each account holds, and Yutai reads its signed word. | Nothing yet: no holdings channel stands for this kind, so Yutai reads no holdings from it. | | What the distributor sends | Wallet addresses: one for each wallet the holder links. Nothing else about the holder. | A pseudonymous user reference for each account, and a short-lived signed statement of its holdings: never a name, an email address or a phone number. | Nothing yet: with no holdings channel, there is nothing for the platform to send about a holding. | | How long a holder has held | The holding's own chain history, which Yutai reads; when no history stands, the first instant Yutai saw the holding at or above the floor. | The day the distributor's signed statement says the holding began. | Not yet: no holding is read for this kind, so no tenure is counted from it. | | How a claim is proven | The holder signs a one-time message Yutai issues for the claim. | The distributor's own signature stands beside the claim. | Not yet: with no holdings channel, a claim has no holding behind it to prove. | --- # Verify a credential A holder can hand you their Yutai credential: the tier they carry and the day it was issued. Paste it below to check it. The check runs in this browser alone, so the credential is never sent to Yutai or anywhere else, and nothing is kept. The proof is simulated for now: no key signs a credential yet, so the check proves the credential has not been changed since Yutai issued it, not who issued it. Credential Paste the whole credential, as the holder handed it to you. --- # Link a customer account A custodial distributor ties one of its customer accounts to one Yutai holder with one signed link grant. The distributor's own registered key signs the grant, and the link is what lets the holdings the distributor attests for that account count for the holder. This page shows the message the grant carries and the rules it must meet. The ids and instants travel in the shapes the [identifiers page](/docs/identifiers) carries. Yutai does not receive link grants yet. These are the contracts the grant will ride, so what you build against them now stays the same. On this page - [The message](#message-heading) - [The account reference](#reference-heading) - [The window](#window-heading) - [The signature](#signature-heading) - [What Yutai does with it](#handling-heading) - [The submission](#submission-heading) ## The message The grant is one fixed message the distributor signs. Yutai builds the same text from the same facts, in the same order, so a signature verified over one text is verified over the other: what the distributor signed is what Yutai read. The message the distributor signs ``` Yutai venue account link Tenant: tnt_01J9ZQ4V8W7X6Y5Z4A3B2C1D0E Venue: tnt_01J9ZQ5B7C8D9E0F1G2H3J4K5M Holder: hld_01J9ZQ6N8P9Q0R1S2T3V4W5X6Y Account: acct-1 Issued: 2026-10-08T09:30:00.000Z Expires: 2026-10-08T09:40:00.000Z ``` The first line is fixed. The `Tenant` line names the member the holder belongs to, the `Venue` line names the distributor's own member id, since a distributor is itself a member of the network, and the `Holder` line names the holder the account links to. The `Account` line carries the account reference, and `Issued` and `Expires` set the window below. ## The account reference The `Account` line carries the distributor's own reference for the customer account, one pseudonymous value: 1 to 128 characters of letters, digits, dot, underscore and hyphen. No email address, no phone number: a distributor that would send its customers' contact details is refused rather than trusted not to. Each refusal below reads Yutai's own words for it. `(empty)`: Refused: `a user_ref is not empty`. `acct:1`: Refused: `a user_ref carries no colon`. `ana@example.com`: Refused: `a user_ref is pseudonymous, never an email address`. `1234567890`: Refused: `a user_ref is pseudonymous, never a phone number`. `acct 1`: Refused: `a user_ref is letters, digits, dot, underscore and hyphen only`. `aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`: Refused: `a user_ref is at most 128 characters`. `acct-1`: Accepted. The message's Account line carries it above. The account line of the grant itself is checked by a wider rule: it allows a colon, and it accepts the digit runs this rule reads as phone numbers. So a reference the rule refuses, one with a colon in it and one that reads as a phone number, rides in a grant the distributor can sign. No holder key names the account such a grant links, so no ledger row keys by it. ## The window The grant carries its own window: the distributor sets `Issued` and `Expires` when it signs, and the grant answers only while it stands inside that window. A window runs at most `10 minutes`, the maximum the contracts fix; the sample grant above runs exactly that long. A longer window is refused: `a link grant stays valid for at most ten minutes`. ## The signature The distributor signs the exact message text with the key Yutai holds for the distributor. One scheme is recorded: `ed25519`, a public-key scheme, so verifying a signature needs only the distributor's registered key and Yutai holds no secret for the distributor. The registered public key: 32 bytes in unpadded base64url, 43 characters. For example `WtTAcVt6sLLyNNMbLG71waqd4xm-PPDE1_KokWDks8k`. The signature: 64 bytes in unpadded base64url, 86 characters. For example `P5ocflstjwSmwenTt_WiyOTWsPKoxuHT9ae5wOLU9qjx49XHuaHz5dfJsaP159nBs6X36dHDtaf54dPFt6nw4g`. ## What Yutai does with it Yutai verifies the signature first, over the exact message text, against the distributor's registered key: the key the submission names by id, or the distributor's first key when it names none. The key must be valid at the grant's issued instant. A grant with no signature, one from a distributor with no registered key, or one whose signature does not verify is refused, and nothing is stored. A verified grant must be inside its own window when it arrives: one that has not started or has expired is refused. The holder it names must be a holder of the member the grant names. One distributor account stands linked to at most one holder at a time, across every member of the network; a revoked link blocks nothing, so a new grant can link the account to another holder. A grant's signature links at most once: submitting the same signed grant again does nothing, even after the link it made was revoked. The link gives the account one holder key, the identity the distributor model's ledger keys it by: `venue:tnt_01J9ZQ5B7C8D9E0F1G2H3J4K5M:acct-1`, built from the grant's own `Venue` and `Account` lines. ## The submission Planned Yutai does not receive link grants yet: no route serves them today. The message and its rules above are already the contracts the grant will ride, so what a distributor builds against them now stays the same. --- # Webhooks What Yutai's webhooks send: the kinds of change they tell a distributor about, what one delivery carries, how often a failed delivery is tried again, and how a delivery is checked. The values on this page are read from Yutai's own rules when the site is built, so they cannot drift from them. Yutai does not deliver to real endpoints yet. The deliveries it records run on simulated data, so no change reaches a real address; the rules on this page are the ones its stored deliveries already follow. ## Kinds of change Every delivery tells a distributor about one change, and every change is one kind. A distributor asks for the kinds of change it wants to hear about; the kinds Yutai's registry names: Tier change `tier_changed`: A holder's tier changed: the credential the holder carries now names another tier. Benefit unlock `benefit_unlocked`: A benefit opened through a tier: the benefit and the tier that opens it. Claim confirmation `claim_confirmed`: A claim a holder made is recorded: the claim and the benefit it was made on. Redemption record `redemption_recorded`: A redemption of a claim is recorded. ## What a delivery carries One delivery carries three facts: the event id, the kind and the payload. The bounds are the contracts' own: `eventId`: The change's own id. It names one change and never changes across attempts, so you can tell a repeat from a new change. It carries 1 to 256 characters of letters, digits, dot, underscore, colon and hyphen. `kind`: The kind of change the delivery tells about: one of the four above. `payload`: The change's own facts as flat entries. A key carries 1 to 64 characters of letters, digits, underscore, dot and hyphen. A value is a string of up to 512 characters, a finite number, a boolean or null. At most 32 entries. Each kind names the payload it carries, as flat entries: Tier change payload ``` { "asOf": "2026-10-08T12:00:00.000Z", "credentialId": "crd_01J9ZQ4V8W7X6Y5Z4A3B2C1D0E", "holderId": "hld_01J9ZQ4V8W7X6Y5Z4A3B2C1D0E", "previousTier": "Holder", "tier": "Steward" } ``` Benefit unlock payload ``` { "benefitId": "bft_01J9ZQ5B7C8D9E0F1G2H3J4K5M", "tier": "Steward" } ``` Claim confirmation payload ``` { "claimId": "clm_01J9ZQ6N8P9Q0R1S2T3V4W5X6Y", "benefitId": "bft_01J9ZQ5B7C8D9E0F1G2H3J4K5M" } ``` Redemption record payload ``` { "redemptionId": "red_01J9ZQ7P9R0S1T2V3W4X5Y6Z8A", "claimId": "clm_01J9ZQ6N8P9Q0R1S2T3V4W5X6Y" } ``` ## Delivery rules How a delivery reaches an endpoint: At least once: A distributor may receive the same change more than once: a retry after an answer that was slow, or a redelivery. Yutai never promises exactly once. One delivery per change: A redelivered change is the same delivery: Yutai keeps one delivery per change and endpoint, so a repeat answers the same delivery with the same payload. Record the event ids you have processed and answer a repeat without acting on it again. No order promised: Changes can arrive in any order, and a later change can arrive before an earlier one after a retry. A distributor that needs the current state reads it from Yutai with the calls the [API reference](/docs/api) names rather than replaying deliveries in arrival order. ## Retries Each attempt answers `delivered` or `failed`: a failed answer carries the reason it failed, a delivered one carries none. A failed attempt is tried again on the schedule below; a delivered one ends the delivery. Attempt 1: when the change is sent Attempt 2: 1 minute later Attempt 3: 5 minutes later Attempt 4: 30 minutes later Attempt 5: 2 hours later A delivery that has spent its 5 attempts stays failed and is not tried again on its own. Once it is, Yutai's team can send it again by hand, at most 3 times, each named beside the operator who pressed it. ## The signature A receiver checks a delivery's signature in the `Yutai-Signature` header. The value carries two parts joined by a comma: `t` is the whole number of unix seconds the delivery was signed at, and `v1` is the signature itself, 64 lowercase hex characters. For example `Yutai-Signature: t=1781000000,v1=3f9a1c7e5b2d8f04a6c1e9d3b7f5a2c8e4d6b0f2a8c6e1d3f5a7b9c0e2d4f6a8`. A receiver checks the timestamp first: only a header whose `t` sits within 300 seconds of the receiver's own clock, either way, is fresh, so a captured delivery cannot be replayed later and pass. The signature binds the delivery's own facts, the one message: The message the signature binds ``` Yutai webhook delivery Tenant: tnt_01J9ZQ4V8W7X6Y5Z4A3B2C1D0E Event: tier-update-1 Kind: tier_changed Endpoint: https://hooks.example.test/yutai Payload: {"asOf":"2026-10-08T12:00:00.000Z","credentialId":"crd_01J9ZQ4V8W7X6Y5Z4A3B2C1D0E","holderId":"hld_01J9ZQ4V8W7X6Y5Z4A3B2C1D0E","previousTier":"Holder","tier":"Steward"} ``` The real signing key is not issued yet. Until it is, the signature a stored delivery carries is the simulated digest proof over the same message, not the header above. --- # Sign a balance statement How a custodial distributor signs the balance statement Yutai stores as its attestation: the two kinds of statement, what one payload carries, the exact message to sign, how fresh a statement must be and the signature Yutai verifies. The formats every value travels in are the [identifiers page](/docs/identifiers)'s: chains, instruments, quantities, instants and the prefixed ids. Yutai does not take statements from real distributors yet. The statements it records run on simulated data, so nothing a real distributor sends reaches it; the rules on this page are the ones its stored statements already follow. ## Kinds of statement Yutai learns your users' tokenized holdings from statements your distributor signs and sends. Every statement is one kind. The kinds Yutai's registry names: Push `push`: A statement your system sends as the account's positions change: it carries the current position of every instrument it names. Snapshot `snapshot`: The account's full statement: it names every instrument the account holds, so anything a push missed is caught. ## What one payload carries One statement carries the facts it binds, under the bounds the contract fixes: `tenantId`: The Yutai member the statement is about: the one whose holder the account belongs to. It is the member's own prefixed id, the identifiers page's form. `venueId`: The Yutai member your distributor runs under: the one whose registered key verifies your signature. `accountRef`: Your distributor's own reference for the account, the one its link to the holder carries. It carries 1 to 128 characters of letters, digits, dot, underscore, colon and hyphen; a statement whose reference carries anything else is refused. `payloadId`: The statement's own id, yours to mint. It carries 1 to 128 characters of letters, digits, dot, underscore, colon and hyphen. One statement per id: a second delivery of the same id with the same signature answers the first statement and writes nothing, and an id sent again with other content is refused. `kind`: One of the two kinds above. `attestedAt`: The instant your system attested the statement: ISO 8601 in UTC, the identifiers page's instants. `positions`: The current position of every instrument the statement names, one line each: the instrument's id, the one the instrument listing carries, and the exact base units you hold, as one whole number. At most 64 lines, each instrument once. `movements`: The account's history, one line per movement. At most 256 lines; a longer history arrives across several statements. Each movement carries its own id (yours, once across your accounts), its kind (the four below), the instrument, the base units it moved and the instant it happened at. No movement happens after the instant the statement attests. One movement carries: Buy `buy`: Tokens moved in on a purchase at your distributor. No wallet is named. Sell `sell`: Tokens moved out on a sale at your distributor. No wallet is named. Transfer in `transfer_in`: Tokens moved in from a wallet: the wallet they came from is named. It may also name the on-chain transaction that carried them. Transfer out `transfer_out`: Tokens moved out to a wallet: the wallet they went to is named. It may also name the on-chain transaction that carried them. ## The message You sign the statement's own message, built from the payload's facts in one fixed order: the header lines, then one `Position` line per position and one `Movement` line per movement, in the payload's own order. Instants are ISO 8601 in UTC and amounts are the exact base-unit digits, so the signer and the verifier build byte-identical text. The message over this page's sample payload: The message the signature binds ``` Yutai venue attestation Tenant: tnt_01J9ZR2C4E6G8H0J2K4M6P8ZAB Venue: tnt_01J9ZR3D5F7H9J1K3M5P7Z9BCD Account: acct-4177 Payload: stmt-1001 Kind: snapshot Attested: 2026-10-09T04:00:00.000Z Position: tki_01J9ZR4E6G8H0J2K4M6P8ZACEX 1250000 Position: tki_01J9ZR5F7H9J1K3M5P7Z9BDFFA 300 Movement: mov-0091 buy tki_01J9ZR4E6G8H0J2K4M6P8ZACEX 250000 2026-10-08T15:12:00.000Z Movement: mov-0092 transfer_in tki_01J9ZR5F7H9J1K3M5P7Z9BDFFA 100 2026-10-08T18:40:00.000Z 4Nd1mBQ6MJ6ViN5jM2Z4atPozHvGvQtME5hkmZjpSXRV ``` ## How fresh a statement must be The freshness window: Yutai accepts a statement at most 300 after the instant it attests. An older one is refused, however it was signed. The clock skew: Your attested instant may sit at most 60 ahead of the instant Yutai receives the statement. Further ahead is refused. The hourly cap: Yutai accepts at most 60 per account per hour, so a backend stuck in a retry loop cannot flood one account. The same statement delivered again inside that hour is answered as the first one and does not count twice. ## The signature You sign the statement's message with the signing half of the key your distributor registered. Yutai verifies every statement against that registered public key before anything is stored; a statement that does not verify is refused and audited, and nothing of it is stored. The scheme: Ed25519, the one scheme the registry carries: a public-key scheme, so Yutai verifies with your registered public key and never holds a signing half. The key: The public half of your signing key: 43 characters of letters, digits, underscore and hyphen, the unpadded base64url of 32 bytes. Only the public half is stored; the signing half stays with you. The signature: The signature of the message above: 86 characters of letters, digits, underscore and hyphen, the unpadded base64url of 64 bytes. The key id: Your statement may name the key that signed it. When it names none, Yutai verifies against your distributor's first key, k1. A key id carries 1 to 32 characters of letters, digits, dot, underscore and hyphen. Replacing the key: When you replace the signing key, the new one starts at the instant you name and the old one keeps verifying until the instant you name too: at most 72 hours after the new one starts. Both verify side by side during that overlap, so a statement signed just before the switch still lands. The one scheme the registry carries: `ed25519`. A second scheme extends the registry's list; a caller's own string is refused. ## What Yutai refuses Every refusal below answers with nothing stored: the statement leaves no trail, and the refusal itself is the only record of it. No signature: A statement that carries no signature is refused. No registered key: A statement from a distributor whose key is not registered is refused. A signature that does not verify: A signature that does not verify over the message above, under the registered key, is refused. The account reference: A reference outside its own rule, the one the field above carries, is refused. The account's link: The account must carry an active link on the statement's member, made no later than the instant the statement attests. A statement about an account with none, or attested before its link took effect, is refused. The distributor's scope: Every instrument the statement names must be one your distributor's own listing carries. A statement naming any instrument outside it is refused whole. The registry: Every instrument must be one the statement's member's registry holds as active. One the registry has paused or retired refuses the whole statement. --- # Credential card The credential card is the one card Yutai's holder app shows: the tier its holder carries, the tenure picture behind it and the verification status of the credential, drawn inside your page from the tokens you set. ## The script Yutai serves the SDK's script at the address below: the script loads once, then the element is written with its facts as attributes. The script is served today. Embed code ``` ``` ## The sample The sample renders the same card from the same element the embed above writes, so the card is live here now, drawn by the served script the script section names. Sample figures: a simulated holder, not a real credential. ## The attributes The card's facts arrive as attributes. The three tenure attributes come as one set: pass all three, or none of them. A Patron card is at the top tier, so it takes tenure-days alone and shows "Top tier reached" with no progress bar. | Attribute | What to pass | | --- | --- | | `tier` | Required. The tier the card carries, one of Holder, Steward or Patron. | | `status` | Required. The verification status the card shows: minted or revoked. | | `tenure-days` | With the tenure set, or alone on a Patron card: the days the credential has been held, a number of days of 0 or more, like "145". | | `tenure-next-tier` | With the tenure set: the tier the tenure is moving toward, one of the tiers above the card's own tier. Not given on a Patron card, the top tier. | | `tenure-progress` | With the tenure set: the progress toward the next tier, a percentage of 0 through 100, like "62.5". Not given on a Patron card, the top tier. | | `tenure-progress-decimals` | Optional. The scale the percentage shows: a whole number of digits, 0 or more, up to the scale the card draws. Not given on a Patron card. | ## The tokens Set the tokens you want on the element itself, the way the member template does: a value the element carries beats the card's own binding. | Token | What it changes | | --- | --- | | `--accent` | The primary action colour. | | `--accent-foreground` | The text colour that sits on the primary action colour. | | `--focus` | The colour focus rings wear. | | `--link` | The colour a link wears. | | `--radius` | The corners the card and its tenure track round with, 0 to 0.75rem. | | `--yutai-font-sans` | The font stack the card's text renders in. | | `--background` | The page background around the card, from the approved neutral list. | Every other token stays Yutai's own: the card's brand colours, its words and its text stay Yutai-controlled, so the card stays accessible on every member's pages. --- # Benefits list The benefits list shows a holder the benefits your distributor offers, one card per benefit: the ones they can claim now, the locked ones with the one sentence that says why, and the ones already claimed or redeemed. It is drawn inside your page from the rows you pass it, in the shape of the distributor API's benefits list. ## The script Yutai serves the SDK's script at the address below: the script loads once, then the element is written with its facts as attributes. The script is served today. Embed code ``` ``` ## The sample The sample renders the same list from the same element and the same three rows the embed above writes: one eligible, one locked, one claimed. Sample rows: a simulated holder's benefits, not a real holder's. ## The attribute The list's facts arrive as one attribute: the rows, in the distributor API's own shape, as JSON text. Set it again with new rows and the list draws them at once. | Attribute | What to pass | | --- | --- | | `benefits` | Required. The holder's benefit rows as a JSON array, at most 500 rows, each with the fields below. The list draws one card per row, in the order given. A row the list cannot draw is refused, naming the row by its position, and the list keeps its last good paint. | ## The row fields Each row carries the fields of one benefit in the distributor API's benefits list, under the names its schema gives them. The list draws the name, the state and the locked sentence, and passes over the rest, so a row can be handed over unchanged. | Field | What it carries | | --- | --- | | `benefitId` | Required. The benefit's id, like "bft_01H8Z2N9P4QX7K3M5A0V8D9T2W". The claim event carries it back. | | `name` | Required. The words the benefit's card names it by. | | `type` | The benefit's type, one of DISCOUNT_CODE, EXPERIENCE, POINTS, VOUCHER, ACCESS, DIGITAL_COLLECTIBLE or STOCK_REWARD. The distributor API's rows carry it; the list does not draw it. | | `delivery` | How the benefit is delivered, one of `CODE`, `POINTS_CREDIT`, `VENUE_REDEMPTION`, `CHAIN_MINT` or `TOKEN_TRANSFER`. The distributor API's rows carry it; the list does not draw it. | | `state` | Required. Where the holder stands with the benefit, one of eligible, locked, claimed, redeemed or unavailable. Only an eligible card carries the claim press. | | `lockedReason` | Required on a locked row, and only there: the one sentence the holder reads about why the benefit is locked. A row in any other state that carries it is refused. | | `sourceMode` | The data mode the row was read in, one of SIMULATED, SANDBOX or LIVE. The distributor API's rows carry it; the list does not draw it. | ## The claim event A press on an eligible card is your distributor's own action: the list tells your page which benefit was pressed, and your page claims it. | Name | What it carries | | --- | --- | | `yutai-claim` | Fired once each time the holder presses the claim on an eligible card. It bubbles and crosses the element's shadow root, so a listener on your document hears it. The list writes nothing itself: the claim is your action. | | `detail.benefitId` | The pressed row's benefitId, exactly as your benefits attribute gave it. | Claim listener ``` document.addEventListener("yutai-claim", (event) => { const { benefitId } = event.detail; // Claim the benefit through your own server, then give the list its new rows. }); ``` The claim itself is your own call: the [API reference](/docs/api) carries the claim calls, the [benefits module](/docs/benefits-module)'s claim sheet runs one for the holder, and [Errors and limits](/docs/errors) explains a refusal. --- # Benefits module The benefits module, `yutai-benefits-module`, draws the whole token picture on one page: one row per token of every wallet its answer carries, each naming the security, the issuer, the wrapper, the chain, the holding and the tier, with the benefits that holding reaches beneath it. Its facts arrive as one of two doors: the wallets attribute, whose ask rides the page's connected client, or the answer attribute, which the module draws without asking anyone. ## The script Yutai serves the SDK's script at the address below: the script loads once, then the module asks through the page's connected client, so call connectDistributorApi with the distributor's key before the module loads. The script is served today. Embed code ``` ``` ## The sample The sample draws the real element here through the answer door: the module carries the answer Yutai's eligibility read carries, as JSON text, and draws it without asking anyone. One wallet of the sample was read, one Yutai has not read yet, so the sample asks for nothing and carries no key. Sample figures: a simulated holder's tokens and benefits, not a real holder's. ## The attributes The module takes the answer or the wallets, never both: a page that names both doors is refused before anything paints. Each attribute carries one of the two doors, under the name the element reads. | Attribute | What to pass | | --- | --- | | `wallets` | The wallet addresses as JSON text, at most 20 addresses in one read. The module asks what the wallets reach through the connected client your page made with `connectDistributorApi`, the one keyed door per page: the module carries no key of its own and fetches nothing on its own path. The refusals section quotes an example of the shape the addresses take. | | `answer` | The eligibility answer as JSON text, the answer Yutai's eligibility read carries: the module draws it instead of asking, so a page whose read happens on its server hands the answer over and no key reaches the browser. | ## The claim A press on an eligible card's claim button opens the claim sheet: the sheet names the benefit, asks Yutai for the one message the holder's wallet signs, and its confirm step draws the safety sentence that message itself carries beside one wallet line each, so a page that hands more than one wallet claims with the one the holder picks. The press signs the challenge once with that wallet's own call and records the claim on that proof, and the sheet draws the claimed word, with the code the answer carried beside it and, on the first grant, a link that opens the holder's own redemption page: the claim's own address the answer carried, never one the sheet composes. A replay of a claim already granted answers the same code and never a link, and a refusal draws neither. The wallets the sheet proves with are your page's own, handed in as the property below: the sheet reads the property at press time, so a page that hands it after the module drew is read the same way a page that handed it before the first paint is. | Property | What to pass | | --- | --- | | `claimWallets` | The wallets the claim sheet proves with, as a JavaScript property on the element, never an attribute: an array of entries, each carrying the wallet's address beside its own `signMessage` call, the call that answers the signature. No key rides an entry; the sign call is the page's own. A press without the property, or with a malformed entry, opens the sheet with the one sentence that names it, and nothing is sent. A module that carries no wallets reads: The benefits module carries no claimWallets wallets to prove the claim with. Set the module's claimWallets property to the page's wallets beside their sign calls, then press Claim again. A first entry that is not a wallet with its sign call reads, naming the entry's position: The claimWallets property carries an entry that is not a wallet with its sign call at position 1. Each entry carries the wallet's address and its own signMessage call. | A granted answer tells your page the claim stands through one event; a refused one fires nothing. | Name | What it carries | | --- | --- | | `yutai-claimed` | Fired once for each granted answer, from the element itself, bubbling and crossing the element's shadow root, so a listener on your document hears it wherever the element sits. An answer marked `alreadyClaimed` is granted too, so it fires the event; a refused answer fires nothing. The module's press opens the sheet instead of dispatching the benefits list's own yutai-claim event; the list keeps its own. | | `detail.claimId` | The id of the claim the sheet's ask recorded. | | `detail.benefitId` | The pressed benefit's id, exactly as the drawn answer carried it. | Claimed listener ``` document.addEventListener("yutai-claimed", (event) => { const { claimId, benefitId } = event.detail; // The claim stands on Yutai's record: update your own records from the two ids. }); ``` ## The refusals The module never paints input it cannot read. A refusal happens before anything renders, so the module keeps its last good paint, and it reaches your page as one yutai-error event, its detail carrying the module's tag and one plain sentence. For a page that names both doors, the sentence is: > The benefits module has both an answer and a wallets attribute. Set the answer or the wallets, never both. For a page that names neither, the sentence is: > The benefits module has no wallets attribute. Set wallets to the wallet addresses as JSON, like ["0xabc0000000000000000000000000000000000001"]. --- # Tier badge The tier badge shows one holder's tier at a glance: the tier its credential carries, and, when you have the figure, the days it has been held. It is drawn inside your page and carries no key and no fetch of its own: a badge that marks an instrument asks through the one client your page connected. ## The script Yutai serves the SDK's script at the address below: the script loads once, then the element is written with its facts as attributes. The script is served today. Embed code ``` ``` ## The sample The sample renders the real element here: one badge per tier, the Holder and Steward badges with their days figures, the Patron badge with the tier's word alone. Sample figures: a simulated holder, not a real credential. ## The attributes The badge has two draws, one per badge: the tier draw when the badge names a tier, the instrument draw when it names an ISIN instead. The tier draw paints at once; the instrument draw asks the distributor API, so it paints nothing while the ask rides. | Attribute | What to pass | | --- | --- | | `tier` | Required on the tier draw. The tier the badge carries, one of Holder, Steward or Patron. A badge names a tier or an instrument, never both. | | `tenure-days` | Optional on the tier draw: the days the credential has been held, a number of days of 0 or more, like "145", the same figure the card's tenure-days carries under the same name. Absent, the badge draws the tier's word alone. | | `isin` | Required on the instrument draw. The ISIN of the instrument the badge marks, written as [the ISIN form](/docs/identifiers#isin-heading): the badge asks the distributor API which benefits the instrument carries, through the connected client your page made with `connectDistributorApi`, and paints the mark that says benefits exist at the company. The tenure-days attribute never rides the instrument draw. | | `answer` | Optional on the instrument draw. The availability read's own answer for that instrument, as JSON text: the badge draws it instead of asking, so a page whose read happens on its server can hand every badge the row the read answered it with and no key reaches the browser. Write a badge only where your read answered; a company the read answers nothing for wears no badge. | Each tier paints its own look from its own token pair: --tier-holder, --tier-steward and --tier-patron, each with its foreground. The pairs are bound on the element itself, so a value the distributor sets on the element beats the badge's pack defaults. ## The refusals The badge never paints input it cannot draw. A refusal happens before anything renders, so the badge keeps its last good paint, and it reaches your page as one yutai-error event, its detail carrying the badge's tag and one plain sentence: a badge with neither a tier nor an isin, a tier the badge does not carry, a days figure that is not a plain decimal or sits below zero, an answer without an isin, a badge that names a tier and an instrument together, tenure-days beside an instrument, an isin that is not an ISIN, an answer that is not JSON, an answer that is not the shape the availability read carries, an answer that answers for another instrument than the badge names, an instrument badge whose page connected the Yutai client with no market, or an instrument badge on a page that has not connected the Yutai client yet. For a tier the badge does not carry, the sentence is: > The tier attribute "gold" is not a tier the badge draws. Set tier to Holder, Steward and Patron. Read [the React page](/docs/react) for the React wrapper, and [the Errors and limits page](/docs/errors) for the plain sentences a refusal carries. --- # React The SDK's React components render Yutai's four embed elements on a page built with React, each component's props named for the attributes its element reads. Install the package, import the component you need, and the samples below show the result. ## The script Yutai serves the SDK's script at the address below: a page built with React does not load it, because the components' import registers the same elements before the first render. The script is served today. Embed code ``` ``` ## The components Install the distributor SDK package ( `@yutai/sdk-web`) with your package manager, then import the components you need from its react door ( `@yutai/sdk-web/react`). The import registers the same elements the script line above registers, before the first render: a React page never writes a define of its own, and it needs no second copy of the SDK. Each component renders its element, never a second card. The props you pass are written on the element as its own attributes, so the element reads its facts on connect the way it does on a page without React. The components cover all four of the SDK's elements. The benefits module's two doors, the wallets and the answer, and its `claimWallets` property stay on the [benefits module's own page](/docs/benefits-module); its component's `onClaimed` prop hears the module's own claimed event. ## The props Each component's props are its element's attributes under their camel case names, listed below in the element's own order. A prop you pass is written on the element under the attribute's own name; a prop you leave out writes no attribute, so the element reads it as missing. The attributes' full words and the tokens each element takes stay on the element pages: the [credential card](/docs/credential-card), the [benefits list](/docs/benefits-list), the [benefits module](/docs/benefits-module) and the [tier badge](/docs/tier-badge). ## The credential card's props | Prop | What to pass | | --- | --- | | `tier` | Required. The tier the card carries, one of Holder, Steward or Patron. | | `status` | Required. The verification status the card shows: minted or revoked. | | `tenureDays` | With the tenure set, or alone on a Patron card: the days the credential has been held, like "145". The tenure comes as one set: pass tenureDays, tenureNextTier and tenureProgress together, or none of them. | | `tenureNextTier` | With the tenure set: the tier the tenure is moving toward, one of the tiers above the card's own tier. Not given on a Patron card, the top tier. | | `tenureProgress` | With the tenure set: the progress toward the next tier, a percentage of 0 through 100, like "62.5". Not given on a Patron card, the top tier. | | `tenureProgressDecimals` | Optional. The scale the percentage shows: a whole number of digits, like "1". Not given on a Patron card. | ## The tier badge's props | Prop | What to pass | | --- | --- | | `tier` | Required on the tier draw. The tier the badge carries, one of Holder, Steward or Patron. A badge names a tier or an instrument, never both. | | `tenureDays` | Optional, with tier on the tier draw: the days the credential has been held, like "145", the same figure the card's tenureDays carries. Absent, the badge draws the tier's word alone. | | `isin` | Required on the instrument draw. The ISIN of the instrument the badge marks: the badge asks the distributor API which benefits the instrument carries, through the client your page connected with connectDistributorApi, and paints the mark that says benefits exist at the company. The tenureDays prop never rides the instrument draw. | | `answer` | Optional on the instrument draw. The availability read's own answer for the instrument, as JSON text: the badge draws the row it carries instead of asking anyone, so a page whose read happened elsewhere hands every badge the row the read answered it with, and no key reaches the browser. Write a badge only where your read answered; a company the read answers nothing for wears no badge. | ## The benefits list's props | Prop | What to pass | | --- | --- | | `benefits` | Required. The benefit rows as an array, at most 500 rows, each in the shape the benefits list page documents. The wrapper serialises the array to the one benefits attribute the element reads, so a page passes its rows and never a string it builds itself. | The benefits list adds one prop the element's attributes do not carry: `onClaim`, called with the benefit's id once per claim event the element dispatches. The claim stays your own action: the component writes nothing itself, and the [benefits list page](/docs/benefits-list) carries the event's words. ## The benefits module's props | Prop | What to pass | | --- | --- | | `wallets` | Optional. The wallet addresses to read, as an array, each once. The wrapper serialises the array to the one wallets attribute, and the element asks through the client your page connected with connectDistributorApi. Pass the wallets or the answer, never both. | | `answer` | Optional. The eligibility answer as JSON text: the element draws the tokens and benefits the answer carries without asking anyone, so a page whose read ran on its own server hands the answer over and no key reaches the browser. Pass the answer or the wallets, never both. | The benefits module's two props are its element's own doors: pass the `wallets` or the `answer`, never both. It adds one prop the element's attributes do not carry: `onClaimed`, called with the claim's id and the benefit's id once per claim the module grants. The component writes nothing itself, and the [benefits module page](/docs/benefits-module) carries the element's own words. ## The samples Each sample below renders through the components above, and its code block pastes whole into a React page: the component imports from the SDK's package, the sample's data stands written out in the block, and on your page you pass the values your holder carries. ## The credential card sample Sample figures: a simulated holder, not a real credential. The credential card's code ``` "use client"; import { CredentialCard } from "@yutai/sdk-web/react"; const card = { "tier": "Holder", "status": "minted", "tenureDays": "145", "tenureNextTier": "Steward", "tenureProgress": "62.5" } as const; export function CredentialCardSample() { return ( ); } ``` ## The benefits list sample Sample rows: a simulated holder's benefits, not a real holder's. The benefits list's code ``` "use client"; import { BenefitsList } from "@yutai/sdk-web/react"; const benefits = [ { "benefitId": "bft_01H8Z2N9P4QX7K3M5A0V8D9T2W", "name": "Season voucher", "type": "VOUCHER", "delivery": "CODE", "state": "eligible", "sourceMode": "SIMULATED" }, { "benefitId": "bft_01H8Z2N9P4QX7K3M5A0V8D9T2X", "name": "Lounge access", "type": "ACCESS", "delivery": "VENUE_REDEMPTION", "state": "locked", "lockedReason": "This benefit opens after 30 days of tenure.", "sourceMode": "SIMULATED" }, { "benefitId": "bft_01H8Z2N9P4QX7K3M5A0V8D9T2Y", "name": "Welcome points", "type": "POINTS", "delivery": "POINTS_CREDIT", "state": "claimed", "sourceMode": "SIMULATED" } ] as const; export function BenefitsListSample() { return ; } ``` ## The benefits module sample Sample figures: a simulated holder's tokens and benefits, not a real holder's. The benefits module's code ``` "use client"; import { BenefitsModule } from "@yutai/sdk-web/react"; const answer = { "wallets": [ { "wallet": "0xabc0000000000000000000000000000000000001", "read": true, "sourceMode": "SIMULATED", "tokens": [ { "issuer": null, "security": { "name": "Sample Global Holdings", "isin": "US0000000009" }, "wrapper": "Sample token programme", "chain": "eip155:88880", "shares": "15", "daysHeld": 452, "tier": "Patron", "benefits": [ { "benefitId": "bft_01K4V9QG000000000000000001", "name": "Sample lounge access", "type": "ACCESS", "delivery": "VENUE_REDEMPTION", "state": "eligible", "illustrative": true }, { "benefitId": "bft_01K4V9QG000000000000000002", "name": "Sample welcome points", "type": "POINTS", "delivery": "POINTS_CREDIT", "state": "locked", "lockedReason": "This benefit opens after 30 days of tenure.", "illustrative": true } ] } ] }, { "wallet": "0xdef0000000000000000000000000000000000002", "read": false, "sourceMode": "SIMULATED", "unreadReason": "Yutai has not read this wallet yet." } ] } as const; export function BenefitsModuleSample() { return ; } ``` ## The tier badge sample Sample figures: a simulated holder, not a real credential. The tier badge's code ``` "use client"; import { TierBadge, type TierBadgeProps } from "@yutai/sdk-web/react"; const badges: TierBadgeProps[] = [ { "tier": "Holder", "tenureDays": "145" }, { "tier": "Steward", "tenureDays": "58" }, { "tier": "Patron" } ]; export function TierBadgeSamples() { return ( <> {badges.map((badge) => ( ))} ); } ``` --- # API reference Every call the distributor API serves, read from the interface description Yutai publishes, one page per call. The whole description is one document: it downloads at [https://api.yutai.com/openapi.json](https://api.yutai.com/openapi.json) so you can import it into your own tools. The formats every value travels in are the [identifiers page](/docs/identifiers)'s. A call marked Callable today is served: it answers at the address its page names. Every call presents a key the platform issued to the distributor, except the ones its own page marks as taking none: those answer before any key exists. A call marked Planned is not served yet: its call answers nothing. Every sample answer is simulated: no real holder's data stands behind it. ## The calls One page per call, in the document's own order: the call's method and address, what it does, and whether Yutai serves it today. [GET /v1/eligibility](/docs/api/getWalletEligibility): GET Read the benefits one or more wallets reach Callable today [GET /v1/availability](/docs/api/getBenefitAvailability): GET Read the benefits one or more instruments carry Callable today [GET /v1/registry/companies](/docs/api/listRegistryCompanies): GET Read the companies one registry holds Callable today [GET /v1/registry/instruments](/docs/api/readRegistryInstrumentFacts): GET Read the facts of one or more listed instruments Callable today [GET /v1/claims/{id}](/docs/api/getClaim): GET Read one claim this distributor recorded Callable today [POST /v1/claims/{id}/link](/docs/api/reissueClaimLink): POST Reissue the redemption link of one claim this distributor recorded Callable today [GET /v1/status](/docs/api/getApiStatus): GET Check the API status Callable today [POST /v1/claims/challenge](/docs/api/requestClaimChallenge): POST Ask for the challenge a wallet signs to claim a benefit Callable today [POST /v1/claims](/docs/api/recordClaim): POST Record a claim a wallet's own signature or a distributor's signed statement stands behind Callable today [GET /v1/fulfilments](/docs/api/listPendingFulfilments): GET List the fulfilments waiting on this distributor Callable today [POST /v1/fulfilments/{id}/confirm](/docs/api/confirmFulfilment): POST Confirm one fulfilment waiting on this distributor Callable today [POST /v1/connect/sessions](/docs/api/createConnectSession): POST Create a Connect session Callable today [GET /v1/connect/sessions/{id}](/docs/api/getConnectSession): GET Read a Connect session Callable today ## The key Every call that takes a key presents it in the Authorization header, as a Bearer key: `Authorization: Bearer ytk_your-key`. A publishable key starts with ytk_pub_ and carries 64 lowercase hex characters after it. A secret key starts with ytk_ and carries 64 lowercase hex characters after it. The placeholder `ytk_your-key` stands for the key you hold; no real key rides these pages. Every call that takes a key presents one scope: the scope the call's own page names. The scopes a key may carry come from one closed list: `holders.resolve`: Resolve the holder a wallet belongs to. `credentials.read`: Read a holder's credential status. `benefits.list`: List a holder's benefits. `eligibility.read`: Read the benefits one or more wallets reach. `availability.read`: List a security's available benefits. `claims.write`: Record a benefit claim. `connect.write`: Create a Connect session and unlink a customer account. `connect.read`: Read Connect sessions and their outcomes. ## Limits Once the key a call presents stands, the call is counted, per key and per scope, in one fixed window of 60 seconds that starts on the minute. A call past the scope's own count is refused without being counted: the refusal answers the one error shape the Errors section carries, and its Retry-After header names the seconds to wait until the next window opens. The [errors and limits page](/docs/errors#limits-heading) carries each scope's count and how long an answer stands. ## Errors Every refused call answers one object with one field, message: the plain sentence that says what did not happen. The sentence carries 1 to 512 characters, so a refusal never carries an id, a code or an exception's text. The sample refusal ``` { "message": "The wallet could not be read." } ``` The [errors and limits page](/docs/errors#statuses-heading) carries every refusal status the document declares, each in its own words. --- # Read the benefits one or more wallets reach One call of the distributor API, read from the interface description Yutai publishes. Every sample answer on this page is simulated: no real holder's data stands behind it. The formats every value travels in are the [identifiers page](/docs/identifiers)'s. ## The call GET `/v1/eligibility` Callable today The key presents the `eligibility.read` scope. ## The parameters `wallets`: Required. The wallet addresses to read: one or more, at most 20 in one read, each an EVM 0x address or a Solana address. It sits in the query. ## What it answers The wallets the request named, each with its tokens and the benefits they reach. The claimed state is the one fact the answer carries at the holder level: a benefit the holder the wallet is linked to has claimed reads claimed on every wallet that carries it, so wallets that share it show one fact about their holder. - `wallets` Required. A list of objects with the fields below. It carries at least 1 and at most 20 items. `Each item` Required. An object with the fields below. `wallet` The wallet address the request named, in the spelling it sent. Required. A string. `read` Whether Yutai read the wallet at all; false when the wallet could not be read. Required. True or false. `sourceMode` The data mode the read answered in: SIMULATED, SANDBOX or LIVE. Required. A string that is one of the values below. `SIMULATED` `SANDBOX` `LIVE` `tokens` The tokens the wallet's own trail answers, each with the benefits it reaches; present when the wallet was read. Optional. A list of objects with the fields below. It carries at most 200 items. `Each item` Required. An object with the fields below. `issuer` The issuer that mints the token, or null when no issuer is recorded. Required. A string. It may also be null. `security` The security the token's mapping names, with its name and its ISIN. Required. An object with the fields below. `name` Required. A string. It carries at least 1 and at most 256 characters. `isin` Required. A string. `wrapper` The wrapper programme that carries the token. Required. A string. It carries at least 1 and at most 128 characters. `chain` The chain the token trades on, as its CAIP-2 id. Required. A string. `shares` The wallet's holding as a decimal string at the token's own decimals, never a JSON number. Required. A string. `daysHeld` Whole days the wallet has held the security without selling; counts toward the tier. Required. A whole number, at least 0. `tier` The programme tier the wallet's own trail answers, or null when no tier stands. Required. A string. It may also be null. `benefits` The benefits the holding reaches, each with its state and its illustrative flag. Required. A list of objects with the fields below. It carries at most 500 items. `Each item` Required. An object with the fields below. `benefitId` Required. A string. `name` Required. A string. It carries at least 1 and at most 128 characters. `type` Required. A string that is one of the values below. `DISCOUNT_CODE` `EXPERIENCE` `POINTS` `VOUCHER` `ACCESS` `DIGITAL_COLLECTIBLE` `STOCK_REWARD` `delivery` Required. A string that is one of the values below. `CODE` `POINTS_CREDIT` `VENUE_REDEMPTION` `CHAIN_MINT` `TOKEN_TRANSFER` `state` Required. A string that is one of the values below. `eligible` `locked` `claimed` `redeemed` `unavailable` `illustrative` Required. True or false. `lockedReason` Optional. A string. It carries at least 1 and at most 512 characters. `unreadReason` Why the wallet could not be read; present only when read is false. Optional. A string. It carries at least 1 and at most 512 characters. ## The errors Every refused call answers one object with one field, message: the plain sentence that says what did not happen. The sentence carries 1 to 512 characters, so a refusal never carries an id, a code or an exception's text. | Status | What it answers | | --- | --- | | 400 | The query could not be read. | | 401 | The key is missing, malformed, unknown, revoked or expired. | | 403 | The key does not carry the scope, or the wallets name chains outside the distributor's scope. The call is also refused with 403, nothing read, when the distributor is paused, suspended or closed, when the key is paused, or when the key presents from an origin it does not allow. | | 429 | The key made too many calls this minute, or the platform has used today's live wallet reads; the Retry-After header names the seconds until the next window opens, or until the reads open again. Nothing was read. | | 500 | The read failed inside Yutai. Nothing was changed. | | 503 | The data source this read needs is not live yet, or the owners' blockchain switch does not allow the read, so the read is refused. Nothing was read. | --- # Read the benefits one or more instruments carry One call of the distributor API, read from the interface description Yutai publishes. Every sample answer on this page is simulated: no real holder's data stands behind it. The formats every value travels in are the [identifiers page](/docs/identifiers)'s. ## The call GET `/v1/availability` Callable today The key presents the `availability.read` scope. ## The parameters `instruments`: Required. The instruments to read: one or more, at most 200 in one read, each an ISIN such as US0000000009 or a CAIP-19 instrument id such as eip155:88880/erc20:0x0000000000000000000000000000000000000000. It sits in the query. `country`: Required. The market whose stored policy shapes the answer: its two-letter country code, such as PT. It sits in the query. ## What it answers The instruments the request named, each once in the order it named them, keyed by its security's ISIN: an instrument the registry does not resolve and a company with no benefit to offer are left out. Each row names the market display policy its shape stands under - full carries the benefits, badge_only the count alone - and the market the ask names shapes every row, full when that market stores no policy. - `instruments` Required. A list of objects with the fields below. It carries at most 200 items. `Each item` Required. An object with the fields below. `isin` The instrument's ISIN, in the registry's canonical spelling. Required. A string. `display` full lists the benefits, badge_only carries the count alone. Required. A string that is one of the values below. `full` `badge_only` `count` How many benefits the company offers in the caller's country. Required. A whole number, from 1 to 500. `illustrative` True when every benefit the count counts is illustrative, meaning an example Yutai made up rather than a real offer. Required. True or false. `benefits` The benefits the count counts, present on a full row and absent on a badge_only row. Optional. A list of objects with the fields below. It carries at most 500 items. `Each item` Required. An object with the fields below. `name` The benefit's name as the company writes it. Required. A string. It carries at least 1 and at most 128 characters. `requirement` What the holder must do to reach the benefit, in the company's own short words. Required. A string. It carries at least 1 and at most 512 characters. `fulfilment` How the benefit reaches the holder: CODE for a discount code or voucher, POINTS_CREDIT for points, VENUE_REDEMPTION for access at the venue, CHAIN_MINT for a digital collectible and TOKEN_TRANSFER for a token sent to the holder's wallet. Required. A string that is one of the values below. `CODE` `POINTS_CREDIT` `VENUE_REDEMPTION` `CHAIN_MINT` `TOKEN_TRANSFER` `illustrative` True when the benefit is an example Yutai made up, not an offer a real company has agreed to. Required. True or false. ## The errors Every refused call answers one object with one field, message: the plain sentence that says what did not happen. The sentence carries 1 to 512 characters, so a refusal never carries an id, a code or an exception's text. | Status | What it answers | | --- | --- | | 400 | The query could not be read. | | 401 | The key is missing, malformed, unknown, revoked or expired. | | 403 | The key does not carry the scope. The call is also refused with 403, nothing read, when the distributor is paused, suspended or closed, when the key is paused, or when the key presents from an origin it does not allow. | | 429 | The key made too many calls this minute. The Retry-After header names the seconds until the next window opens. Nothing was read. | | 500 | The read failed inside Yutai. Nothing was changed. | | 503 | The data source this read needs is not live yet, so the read is refused. Nothing was read. | --- # Read the companies one registry holds One call of the distributor API, read from the interface description Yutai publishes. Every sample answer on this page is simulated: no real holder's data stands behind it. The formats every value travels in are the [identifiers page](/docs/identifiers)'s. ## The call GET `/v1/registry/companies` Callable today The key presents the `availability.read` scope. ## The parameters `limit`: Optional. The page's size: from 1 to 200 rows, 50 when left out. It sits in the query. `cursor`: Optional. The page link the previous answer carried: the nextCursor field's value of the page before this one. It sits in the query. `country`: Optional. Narrow the page to the companies of one country: its two-letter country code, such as US. It sits in the query. `isin`: Optional. Narrow the page to the company the ISIN belongs to: the share class's ISIN, such as US0000000009. It sits in the query. `chain`: Optional. Narrow the page to the companies with an instrument on the chain: its CAIP-2 id, such as eip155:88880. It sits in the query. ## What it answers The companies one registry holds today, each once in key order: the company's name, its country and the mode its rows are in, then its share classes, each with the ACTIVE token instruments its mapping resolves to, named by CAIP-19 id, chain, wrapper name and the token issuer's name, which stands null where the programme names none. Each instrument carries its programme's facts too: its legal form, whether the issuing company sponsors it, its transfer model, whether holding it needs the programme's own KYC, and the token it converts into, named by its CAIP-19 id and programme. A form or a model the registry has not recorded reads null, and the conversion reads null where the registry stores none or the target is not itself listed. Only what a venue lists an instrument by rides: no internal id, no tenant and no holder fact. The next page's cursor rides beside the rows, null when this is the last page. - `items` Required. A list of objects with the fields below. It carries at most 200 items. `Each item` Required. An object with the fields below. `name` Required. A string. It carries at least 1 and at most 256 characters. `country` Required. A string. `sourceMode` Required. A string that is one of the values below. `SIMULATED` `SANDBOX` `LIVE` `securities` Required. A list of objects with the fields below. It carries at least 1 item. `Each item` Required. An object with the fields below. `isin` Required. A string. `ticker` Required. A string. `instruments` Required. A list of objects with the fields below. It carries at least 1 item. `Each item` Required. An object with the fields below. `instrumentId` Required. A string. `chain` Required. A string. `wrapperName` Required. A string. It carries at least 1 and at most 128 characters. `tokenIssuerName` Required. A string. It carries at least 1 and at most 256 characters. It may also be null. `legalForm` Required. A string that is one of the values below. It may also be null. `synthetic_tracker` `security_entitlement_ucc8` `native_registered_share` `other` `issuerSponsored` Required. True or false. `transferModel` Required. A string that is one of the values below. It may also be null. `open` `permissioned_whitelist` `kycByWrapper` Required. True or false. `convertsTo` Required. An object with the fields below. It may also be null. `instrumentId` Required. A string. `wrapperName` Required. A string. It carries at least 1 and at most 128 characters. - `nextCursor` Required. A string. It may also be null. ## The errors Every refused call answers one object with one field, message: the plain sentence that says what did not happen. The sentence carries 1 to 512 characters, so a refusal never carries an id, a code or an exception's text. | Status | What it answers | | --- | --- | | 400 | The query could not be read. | | 401 | The key is missing, malformed, unknown, revoked or expired. | | 403 | The key does not carry the scope. The call is also refused with 403, nothing read, when the distributor is paused, suspended or closed, when the key is paused, or when the key presents from an origin it does not allow. | | 429 | The key made too many calls this minute. The Retry-After header names the seconds until the next window opens. Nothing was read. | | 500 | The read failed inside Yutai. Nothing was changed. | | 503 | The data source this read needs is not live yet, so the read is refused. Nothing was read. | --- # Read the facts of one or more listed instruments One call of the distributor API, read from the interface description Yutai publishes. Every sample answer on this page is simulated: no real holder's data stands behind it. The formats every value travels in are the [identifiers page](/docs/identifiers)'s. ## The call GET `/v1/registry/instruments` Callable today The key presents the `availability.read` scope. ## The parameters `instrumentIds`: Required. The instruments to read the facts of: one or more, at most 100 in one read, each a CAIP-19 instrument id such as eip155:88880/erc20:0x0000000000000000000000000000000000000000. It sits in the query. ## What it answers The instruments the ask named, each once in the order it named them: each row names the instrument by its CAIP-19 id, its chain, its wrapper programme and the token issuer's name, which stands null where the programme names none, and carries the programme's own facts - its legal form, whether the issuing company sponsors it, its transfer model, whether holding it needs the programme's own KYC - and the token it converts into, named by its CAIP-19 id and programme. An instrument the ask names that the registry does not list today answers nothing: an unknown, paused, unmapped, other-mode or other-tenant id carries no row. The next page's cursor is null: the ask is answered whole, one call. - `items` Required. A list of objects with the fields below. It carries at most 200 items. `Each item` Required. An object with the fields below. `instrumentId` Required. A string. `chain` Required. A string. `wrapperName` Required. A string. It carries at least 1 and at most 128 characters. `tokenIssuerName` Required. A string. It carries at least 1 and at most 256 characters. It may also be null. `legalForm` Required. A string that is one of the values below. It may also be null. `synthetic_tracker` `security_entitlement_ucc8` `native_registered_share` `other` `issuerSponsored` Required. True or false. `transferModel` Required. A string that is one of the values below. It may also be null. `open` `permissioned_whitelist` `kycByWrapper` Required. True or false. `convertsTo` Required. An object with the fields below. It may also be null. `instrumentId` Required. A string. `wrapperName` Required. A string. It carries at least 1 and at most 128 characters. - `nextCursor` Required. A string. It may also be null. ## The errors Every refused call answers one object with one field, message: the plain sentence that says what did not happen. The sentence carries 1 to 512 characters, so a refusal never carries an id, a code or an exception's text. | Status | What it answers | | --- | --- | | 400 | The query could not be read. | | 401 | The key is missing, malformed, unknown, revoked or expired. | | 403 | The key does not carry the scope. The call is also refused with 403, nothing read, when the distributor is paused, suspended or closed, when the key is paused, or when the key presents from an origin it does not allow. | | 429 | The key made too many calls this minute. The Retry-After header names the seconds until the next window opens. Nothing was read. | | 500 | The read failed inside Yutai. Nothing was changed. | | 503 | The data source this read needs is not live yet, so the read is refused. Nothing was read. | --- # Read one claim this distributor recorded One call of the distributor API, read from the interface description Yutai publishes. Every sample answer on this page is simulated: no real holder's data stands behind it. The formats every value travels in are the [identifiers page](/docs/identifiers)'s. ## The call GET `/v1/claims/{id}` Callable today The key presents the `claims.write` scope. ## The parameters `id`: Required. The claim's own id, the one the claim answer carried: a clm_ prefixed ULID. It sits in the call's address. ## What it answers The claim's own facts: its id, the benefit it claimed and the instant it was recorded, with its state in words - granted, or, when the benefit is one the venue credits itself, the venue credit's state (waiting on the venue, or credited once the venue confirmed), or rejected, or voided, when the grant was taken back. Nothing personal rides the answer. - `claimId` The claim's own opaque id, the address the claim read answers at. Required. A string. - `benefitId` The benefit the claim was recorded for, by its own opaque id. Required. A string. - `benefitName` The benefit's name as the programme carries it, so a claim reads without a second lookup. Required. A string. It carries at least 1 and at most 128 characters. - `state` Where the claim stands in plain words: whether the grant stands, was refused or taken back, or the venue's credit is still to come. Required. A string that is one of the values below. `granted` `rejected` `voided` `waiting on the venue` `credited by the venue` - `createdAt` The instant Yutai recorded the claim and its decision. Required. An instant in the ISO 8601 form. ## The errors Every refused call answers one object with one field, message: the plain sentence that says what did not happen. The sentence carries 1 to 512 characters, so a refusal never carries an id, a code or an exception's text. | Status | What it answers | | --- | --- | | 401 | The key is missing, malformed, unknown, revoked or expired. | | 403 | The key does not carry the scope. The call is also refused with 403, nothing read, when the distributor is paused, suspended or closed, when the key is paused, or when the key presents from an origin it does not allow. | | 404 | No claim answers this id under this distributor's key: an unknown id, or one another distributor's key recorded. The answer is the same for both, so no claim id can be probed. | | 429 | The key made too many calls this minute. The Retry-After header names the seconds until the next window opens. Nothing was read. | | 500 | The read failed inside Yutai. Nothing was changed. | --- # Reissue the redemption link of one claim this distributor recorded One call of the distributor API, read from the interface description Yutai publishes. Every sample answer on this page is simulated: no real holder's data stands behind it. The formats every value travels in are the [identifiers page](/docs/identifiers)'s. ## The call POST `/v1/claims/{id}/link` Callable today The key presents the `claims.write` scope. ## The parameters `id`: Required. The claim's own id, the one the claim answer carried: a clm_ prefixed ULID. It sits in the call's address. ## What it answers The fresh link: the claim's own id and the one absolute address its new token opens, built on the same public origin the granted answer names. Only the last link reissued opens the claim's page, so the previous link stops working the moment this answer leaves; the token itself is never stored, so this answer carries the only copy. - `claimId` The claim's own id, the one the claim answer carried. Required. A string. - `link` The one absolute address where the holder finishes the claim. Required. A string. It carries at least 1 and at most 512 characters. ## The errors Every refused call answers one object with one field, message: the plain sentence that says what did not happen. The sentence carries 1 to 512 characters, so a refusal never carries an id, a code or an exception's text. | Status | What it answers | | --- | --- | | 401 | The key is missing, malformed, unknown, revoked or expired. | | 403 | The key does not carry the scope. The call is also refused with 403, nothing read, when the distributor is paused, suspended or closed, when the key is paused, or when the key presents from an origin it does not allow. | | 404 | No claim answers this id under this distributor's key: an unknown id, or one another distributor's key recorded. The answer is the same for both, so no claim id can be probed. | | 409 | The claim the id names stands rejected or taken back, so it carries no redemption link to reissue. | | 429 | The key made too many calls this minute. The Retry-After header names the seconds until the next window opens. Nothing was read. | | 500 | The read failed inside Yutai. Nothing was changed. | --- # Check the API status One call of the distributor API, read from the interface description Yutai publishes. Every sample answer on this page is simulated: no real holder's data stands behind it. The formats every value travels in are the [identifiers page](/docs/identifiers)'s. ## The call GET `/v1/status` Callable today This call takes no key: you can call it before you hold one. ## What it answers Every external domain of Yutai's data, each with its mode today and the one sentence to show beside data in that mode. The call needs no key, because a distributor calls it before it holds one. - `apiVersion` Required. It always carries the value below. `1` - `domains` Required. A list of objects with the fields below. `Each item` Required. An object with the fields below. `domain` Required. A string that is one of the values below. `chain` `openbanking` `enrichment` `prices` `wallet` `kyc-attest` `registry` `token-issuer-sources` `reference-data` `custodial` `treasury` `mode` Required. A string that is one of the values below. `SIMULATED` `SANDBOX` `LIVE` `sentence` Required. A string. It carries at least 1 character. ## The errors Every refused call answers one object with one field, message: the plain sentence that says what did not happen. The sentence carries 1 to 512 characters, so a refusal never carries an id, a code or an exception's text. | Status | What it answers | | --- | --- | | 500 | The read failed inside Yutai. Nothing was changed. | --- # Ask for the challenge a wallet signs to claim a benefit One call of the distributor API, read from the interface description Yutai publishes. Every sample answer on this page is simulated: no real holder's data stands behind it. The formats every value travels in are the [identifiers page](/docs/identifiers)'s. ## The call POST `/v1/claims/challenge` Callable today The key presents the `claims.write` scope. ## The body `benefitId`: Required. The benefit the claim stands on: its id, the one the eligibility read named. `wallets`: Required. The wallets the challenge binds the proof to: one or more, at most 20 in one ask. ## What it answers The challenge: the exact message the holder signs, the one-time nonce to hand the page beside it, the wallets the proof binds to, and the instant the window closes, 300 seconds after the call. The signature proves the wallet owns the address; it moves nothing and approves nothing. - `message` The exact text the holder's wallet signs. Signing moves nothing and approves nothing. Required. A string. It carries at least 1 and at most 2048 characters. - `nonce` The one-time nonce to hand the holder's page beside the message: single use, and Yutai stores only its digest. Required. A string. - `wallets` The wallets the proof binds to, each once, the same list the request named. Required. A list of strings. It carries at least 1 and at most 20 items. - `expiresAt` The instant the challenge stops being accepted, counted from the moment the door issued it. Required. An instant in the ISO 8601 form. ## The errors Every refused call answers one object with one field, message: the plain sentence that says what did not happen. The sentence carries 1 to 512 characters, so a refusal never carries an id, a code or an exception's text. | Status | What it answers | | --- | --- | | 400 | The request body could not be read. | | 401 | The key is missing, malformed, unknown, revoked or expired. | | 403 | The key does not carry the scope, or the challenge itself was refused: the venue is paused or not a self-custody venue, the benefit is not a claimable offer, or a wallet sits on a chain the venue does not serve. The call is also refused with 403, nothing read, when the distributor is paused, suspended or closed, when the key is paused, or when the key presents from an origin it does not allow. | | 429 | The key made too many calls this minute. The Retry-After header names the seconds until the next window opens. Nothing was read. | | 500 | The read failed inside Yutai. Nothing was changed. | --- # Record a claim a wallet's own signature or a distributor's signed statement stands behind One call of the distributor API, read from the interface description Yutai publishes. Every sample answer on this page is simulated: no real holder's data stands behind it. The formats every value travels in are the [identifiers page](/docs/identifiers)'s. ## The call POST `/v1/claims` Callable today The key presents the `claims.write` scope. ## The body `benefitId`: The wallet proof: the benefit the claim stands on, the wallet that signs the challenge, the challenge's one-time nonce and the signature over the challenge's own message. Required. The benefit the claim stands on: its id, the one the eligibility read named. `wallet`: The wallet proof: the benefit the claim stands on, the wallet that signs the challenge, the challenge's one-time nonce and the signature over the challenge's own message. Required. The wallet that signs the claim: the one address the challenge named, EVM or Solana. `nonce`: The wallet proof: the benefit the claim stands on, the wallet that signs the challenge, the challenge's one-time nonce and the signature over the challenge's own message. Required. The challenge's one-time nonce, the one the challenge call returned once. `signature`: The wallet proof: the benefit the claim stands on, the wallet that signs the challenge, the challenge's one-time nonce and the signature over the challenge's own message. Required. The proof over the challenge's message, as the wallet signed it. Yutai re-reads every rule at claim time. `benefitId`: The attested ask: a custodial distributor claims a benefit for its own customer by presenting the signed statement its own key signs. Yutai re-checks the statement and every rule at claim time, so the answer reflects the world as it stands. Required. The benefit the claim stands on: its id, the one the eligibility read named. `subject`: The attested ask: a custodial distributor claims a benefit for its own customer by presenting the signed statement its own key signs. Yutai re-checks the statement and every rule at claim time, so the answer reflects the world as it stands. Required. The attested subject the claim is about: the one subject the eligibility read's request carries. The distributor's own key presents the statement its own system signed, with the id of the registered key that verified it, the instant the call was made, and the venue and the account the statement binds, each once; the request's own fields must name the statement's own venue and account, and Yutai re-checks the statement and every rule at claim time. The statement's own shapes stand in the contracts beside the wallet read's and on the sign a balance statement page, which documents the exact message the signature binds; this document names no balance. ## What it answers The claim's answer: granted, with the claim's own facts - its id, the wallets that signed it, the instant it was recorded and the decision behind it - or refused, when the holder's own world refused the claim, with the reason beside the same decision. The granted answer carries the code the benefit delivers, when it delivers by code, and the holder's private redemption link, on the first grant only. A retry of the same challenge answers the one claim it already recorded, marked alreadyClaimed, and so does a statement whose account already stands for the claim. The answer is one of 2 shapes: the fields each shape carries stand under it. - `status` The granted answer's own word: always claimed. Required. It always carries the value below. `claimed` - `claimId` The claim's own opaque id, the address the claim read answers at. Required. A string. - `benefitId` The benefit the claim was granted for, by its own opaque id. Required. A string. - `venueId` The venue that asked, by its own tenant id; no other venue's claim answers it. Required. A string. - `subjectKeys` The wallets the claim's challenge named, the ones the venue itself saw; never the holder's other wallets. Required. A list of strings. It carries at most 64 items. - `alreadyClaimed` True when this call replayed a claim already recorded and false on the first grant; the same call answers the same words twice. Required. True or false. - `createdAt` The instant Yutai recorded the claim and its decision, as an ISO 8601 string. Required. An instant in the ISO 8601 form. - `code` The code the holder redeems, present only when the benefit delivers by code; a replay answers the same code, never a second take. Optional. A string. It carries at least 1 and at most 128 characters. - `link` The holder's private redemption link, present on the first grant only; a replay carries no link. Optional. A string. It carries at least 1 and at most 512 characters. - `decision` The claim's own decision: whether the benefit's rules granted it, with only the notes the eligibility read and the challenge already answer a venue. Required. An object with the fields below. `eligible` Required. True or false. `tree` Required. A list of objects with the fields below. It carries at most 64 items. `Each item` Required. An object with the fields below. `kind` Required. A string that is one of the values below. `tenure` `holding` `country` `spend` `consent` `venue` `holds` `holds_any` `holds_all` `holds_count` `window` `stock` `authorisation_active` `not_paused` `recipient_evm` `not_already_claimed` `attestation_given` `jurisdiction` `recipient_rules` `met` Required. True or false. `note` Required. A string. It carries at least 1 and at most 512 characters. `simulatedInputs` Optional. A list of objects with the fields below. It carries at most 576 items. `Each item` Required. An object with the fields below. `kind` Required. A string that is one of the values below. `tenure` `position` `country` `spend` `consent` `venue` `holding` `price` `stock` `securityId` Optional. A string. It carries at least 1 and at most 256 characters. `children` Optional. A list of the same shapes the tree carries above. It carries at most 64 items. `simulatedInputs` Optional. A list of objects with the fields below. It carries at most 576 items. `Each item` Required. An object with the fields below. `kind` Required. A string that is one of the values below. `tenure` `position` `country` `spend` `consent` `venue` `holding` `price` `stock` `securityId` Optional. A string. It carries at least 1 and at most 256 characters. - `status` The refused answer's own word: always refused. Required. It always carries the value below. `refused` - `reason` Why the claim was refused, one of the closed list of plain reasons a venue branches on. Required. A string that is one of the values below. `unknown_challenge` `other_venue` `other_benefit` `not_consumed` `no_holder` `conflicting_holders` `unknown_benefit` `no_offer` `already_claimed` `not_eligible` - `decision` The benefit's decision, present only when the world itself refused (the rulebook, the holdings trail or the country); an already claimed refusal carries none. Optional. An object with the fields below. `eligible` Required. True or false. `tree` Required. A list of objects with the fields below. It carries at most 64 items. `Each item` Required. An object with the fields below. `kind` Required. A string that is one of the values below. `tenure` `holding` `country` `spend` `consent` `venue` `holds` `holds_any` `holds_all` `holds_count` `window` `stock` `authorisation_active` `not_paused` `recipient_evm` `not_already_claimed` `attestation_given` `jurisdiction` `recipient_rules` `met` Required. True or false. `note` Required. A string. It carries at least 1 and at most 512 characters. `simulatedInputs` Optional. A list of objects with the fields below. It carries at most 576 items. `Each item` Required. An object with the fields below. `kind` Required. A string that is one of the values below. `tenure` `position` `country` `spend` `consent` `venue` `holding` `price` `stock` `securityId` Optional. A string. It carries at least 1 and at most 256 characters. `children` Optional. A list of the same shapes the tree carries above. It carries at most 64 items. `simulatedInputs` Optional. A list of objects with the fields below. It carries at most 576 items. `Each item` Required. An object with the fields below. `kind` Required. A string that is one of the values below. `tenure` `position` `country` `spend` `consent` `venue` `holding` `price` `stock` `securityId` Optional. A string. It carries at least 1 and at most 256 characters. ## The errors Every refused call answers one object with one field, message: the plain sentence that says what did not happen. The sentence carries 1 to 512 characters, so a refusal never carries an id, a code or an exception's text. | Status | What it answers | | --- | --- | | 400 | The request body could not be read. | | 401 | The key is missing, malformed, unknown, revoked or expired. | | 403 | The key does not carry the scope, or the claim itself was refused: the challenge was never verified, was issued for another distributor or another benefit, or the holder's own world refused the claim, and the attested subject's statement the same - unsigned or unverifiable, expired, naming an instrument the distributor does not list or the registry no longer keeps active, naming another tenant, or for a benefit the account's holder already claimed once. The call is also refused with 403, nothing read, when the distributor is paused, suspended or closed, when the key is paused, or when the key presents from an origin it does not allow. | | 429 | The key made too many calls this minute. The Retry-After header names the seconds until the next window opens. Nothing was read. | | 500 | The read failed inside Yutai. Nothing was changed. | --- # List the fulfilments waiting on this distributor One call of the distributor API, read from the interface description Yutai publishes. Every sample answer on this page is simulated: no real holder's data stands behind it. The formats every value travels in are the [identifiers page](/docs/identifiers)'s. ## The call GET `/v1/fulfilments` Callable today The key presents the `claims.write` scope. ## What it answers The calling distributor's own pending fulfilments, newest first: each names the claim it opens beside, the benefit it credits and the instant it was opened, with the waiting state the pending row carries. Nothing personal rides the answer. - `fulfilments` Required. A list of objects with the fields below. `Each item` Required. An object with the fields below. `fulfilmentId` The fulfilment's own opaque id, the address its confirm answers at. Required. A string. `claimId` The claim this fulfilment opens beside, by its own opaque id. Required. A string. `benefitId` The benefit this fulfilment credits, by its own opaque id. Required. A string. `benefitName` The benefit's name as the programme carries it, so a fulfilment reads without a second lookup. Required. A string. It carries at least 1 and at most 128 characters. `state` Where the fulfilment stands in plain words: waiting on the venue until its confirm lands, then credited by the venue. Required. A string that is one of the values below. `waiting on the venue` `credited by the venue` `createdAt` The instant the fulfilment was opened, the moment the credit began waiting on the venue. Required. An instant in the ISO 8601 form. `venueReference` The venue's own reference for the credit, one to the stored maximum characters; present only once credited. Optional. A string. It carries at least 1 and at most 256 characters. `confirmedAt` The instant the venue's confirm was recorded; present only once the row is credited by the venue. Optional. An instant in the ISO 8601 form. ## The errors Every refused call answers one object with one field, message: the plain sentence that says what did not happen. The sentence carries 1 to 512 characters, so a refusal never carries an id, a code or an exception's text. | Status | What it answers | | --- | --- | | 401 | The key is missing, malformed, unknown, revoked or expired. | | 403 | The key does not carry the scope. The call is also refused with 403, nothing read, when the distributor is paused, suspended or closed, when the key is paused, or when the key presents from an origin it does not allow. | | 429 | The key made too many calls this minute. The Retry-After header names the seconds until the next window opens. Nothing was read. | | 500 | The read failed inside Yutai. Nothing was changed. | --- # Confirm one fulfilment waiting on this distributor One call of the distributor API, read from the interface description Yutai publishes. Every sample answer on this page is simulated: no real holder's data stands behind it. The formats every value travels in are the [identifiers page](/docs/identifiers)'s. ## The call POST `/v1/fulfilments/{id}/confirm` Callable today The key presents the `claims.write` scope. ## The parameters `id`: Required. The fulfilment's own id, the one the poll's answer carried: a ful_ prefixed ULID. It sits in the call's address. ## The body `venueReference`: Required. The reference the credit carries: the words the distributor can point back at in its own records. ## What it answers The fulfilment's own facts after the confirm: the claim it opens beside, the benefit it credits and the instant it was opened, the credited state, the distributor's own reference for the credit and the instant the confirm recorded. Nothing personal rides the answer. - `fulfilmentId` The fulfilment's own opaque id, the address its confirm answers at. Required. A string. - `claimId` The claim this fulfilment opens beside, by its own opaque id. Required. A string. - `benefitId` The benefit this fulfilment credits, by its own opaque id. Required. A string. - `benefitName` The benefit's name as the programme carries it, so a fulfilment reads without a second lookup. Required. A string. It carries at least 1 and at most 128 characters. - `state` Where the fulfilment stands in plain words: waiting on the venue until its confirm lands, then credited by the venue. Required. A string that is one of the values below. `waiting on the venue` `credited by the venue` - `createdAt` The instant the fulfilment was opened, the moment the credit began waiting on the venue. Required. An instant in the ISO 8601 form. - `venueReference` The venue's own reference for the credit, one to the stored maximum characters; present only once credited. Optional. A string. It carries at least 1 and at most 256 characters. - `confirmedAt` The instant the venue's confirm was recorded; present only once the row is credited by the venue. Optional. An instant in the ISO 8601 form. ## The errors Every refused call answers one object with one field, message: the plain sentence that says what did not happen. The sentence carries 1 to 512 characters, so a refusal never carries an id, a code or an exception's text. | Status | What it answers | | --- | --- | | 400 | The request body could not be read. | | 401 | The key is missing, malformed, unknown, revoked or expired. | | 403 | The key does not carry the scope, or the fulfilment was already confirmed: the confirm that stands is the one that landed first. The call is also refused with 403, nothing read, when the distributor is paused, suspended or closed, when the key is paused, or when the key presents from an origin it does not allow. | | 404 | No fulfilment answers this id under this distributor's key: an unknown id, or one another distributor's key recorded. The answer is the same for both, so no fulfilment id can be probed. | | 429 | The key made too many calls this minute. The Retry-After header names the seconds until the next window opens. Nothing was read. | | 500 | The read failed inside Yutai. Nothing was changed. | --- # Create a Connect session One call of the distributor API, read from the interface description Yutai publishes. Every sample answer on this page is simulated: no real holder's data stands behind it. The formats every value travels in are the [identifiers page](/docs/identifiers)'s. ## The call POST `/v1/connect/sessions` Callable today The key presents the `connect.write` scope. ## The body `externalRef`: Required. Your own opaque id for the person the session is for. It is never an email address or a phone number. `returnUrl`: Required. Where the hosted page sends the person back: an https address on your allowed origins, or your app's own scheme when your profile lists it. `context`: Optional. The brand the person came from, named by exactly one of an ISIN or a company id. Leave it out when there is none. `locale`: Optional. The language the hosted page speaks, as a language tag such as en-GB. `mode`: Optional. How the result returns: redirect brings the browser back, popup answers the page that opened it, and app uses the phone's in-app browser. `view`: Optional. What the hosted page shows: consent for a new link, or purchases to read and manage a link that already stands. `wallets`: Optional. The holder's wallet addresses to read on chain, each once, at most 20 in one session. ## What it answers The session's id, the hosted page's address the distributor's app opens, and the instant its one-time token expires, 900 seconds after the call. The address carries the token once; the store keeps only its digest. - `id` Required. A string. - `connectUrl` Required. A string. - `expiresAt` Required. An instant in the ISO 8601 form. ## The errors Every refused call answers one object with one field, message: the plain sentence that says what did not happen. The sentence carries 1 to 512 characters, so a refusal never carries an id, a code or an exception's text. | Status | What it answers | | --- | --- | | 400 | The request body could not be read. | | 401 | The key is missing, malformed, unknown, revoked or expired. | | 403 | The key does not carry the scope, or the return address is not one the distributor's profile allows. The call is also refused with 403, nothing read, when the distributor is paused, suspended or closed, when the key is paused, or when the key presents from an origin it does not allow. | | 429 | The key made too many calls this minute. The Retry-After header names the seconds until the next window opens. Nothing was read. | | 500 | The read failed inside Yutai. Nothing was changed. | --- # Read a Connect session One call of the distributor API, read from the interface description Yutai publishes. Every sample answer on this page is simulated: no real holder's data stands behind it. The formats every value travels in are the [identifiers page](/docs/identifiers)'s. ## The call GET `/v1/connect/sessions/{id}` Callable today The key presents the `connect.read` scope. ## The parameters `id`: Required. The Connect session's own id, the one the create answer carried. It sits in the call's address. ## What it answers The session's standing - pending, opened, connected, cancelled, failed or expired - the distributor's own reference, when it was created and when it ended, and the one reason a failed session carries. A session still open carries no completion and no reason; a session no one opened before its token ran out reads expired. - `id` Required. A string. - `status` Required. A string that is one of the values below. `pending` `opened` `connected` `cancelled` `failed` `expired` - `externalRef` Required. A string. - `createdAt` Required. An instant in the ISO 8601 form. - `completedAt` Required. An instant in the ISO 8601 form. It may also be null. - `failureReason` Required. A string that is one of the values below. It may also be null. `consent_declined` `bank_unavailable` `country_not_supported` ## The errors Every refused call answers one object with one field, message: the plain sentence that says what did not happen. The sentence carries 1 to 512 characters, so a refusal never carries an id, a code or an exception's text. | Status | What it answers | | --- | --- | | 401 | The key is missing, malformed, unknown, revoked or expired. | | 403 | The key does not carry the scope. The call is also refused with 403, nothing read, when the distributor is paused, suspended or closed, when the key is paused, or when the key presents from an origin it does not allow. | | 404 | No Connect session answers this id under this distributor's key: an unknown id, a malformed id, or one another distributor's key created. The answer is the same for both, so no session id can be probed. | | 429 | The key made too many calls this minute. The Retry-After header names the seconds until the next window opens. Nothing was read. | | 500 | The read failed inside Yutai. Nothing was changed. | --- # Example wallet One wallet: the code below is the code this page runs on its server, and the example beneath it shows what the distributor API answered for the wallet named in that code when the page was served, or why nothing was read. ## The call The printed call is the one this page's read makes when it reads: it connects the SDK's client and asks the eligibility read for one wallet. The printed code carries a placeholder where the key stands; this page's own key never reaches it or your browser. The call the example makes ``` import { connectDistributorApi } from "@yutai/sdk-web/client"; const client = connectDistributorApi({ base: "https://api.yutai.com", key: "ytk_your-key" }); const answer = await client.askEligibility(["0xcd8a4c3f9d3ab5c8817729a4d1420b95869b5928"]); ``` - `GET /v1/eligibility` the tokens a wallet holds and the benefits they reach: the read the example's answer draws and the paste box asks - `POST /v1/claims/challenge` the words the wallet signs, before the claim is asked - `POST /v1/claims` the claim, recorded on the proof the wallet signed - `GET /v1/claims/{id}` the claim's own state, in the words the read answers ## The example The live example is not wired yet: its key is not set, so nothing was read. ## The claim flow A benefit the wallet reaches is one its holder can claim. The claim flow is the wallet's own: its wallet signs the one message Yutai asks for, Yutai records the claim on that proof, and the read answers the state the claim stands in. Each press names the way its own benefit reaches the holder: a code reaches the holder in their own Yutai app, and the answers Yutai hands back carry the claim's facts alone, never the holder's code. The flow's own code, the order the presses run it in: the two asks the SDK's client carries, then the read, the calls the panel above names. The flow the example runs ``` import { connectDistributorApi, refusedSentenceOf } from "@yutai/sdk-web/client"; const key = "ytk_your-key"; const client = connectDistributorApi({ base: "https://api.yutai.com", key }); const headers = { Authorization: `Bearer ${key}`, Accept: "application/json" }; // The words the wallet signs, before the claim is asked. const challenge = await client.askClaimChallenge(benefitId, [wallet]); const signature = await wallet.signMessage(challenge.message); // The claim, recorded on that proof. const answer = await client.askClaim({ benefitId, wallet, nonce: challenge.nonce, signature }); if (answer.status !== "claimed") throw new Error(refusedSentenceOf(answer.reason)); // The claim's own state, in the words the read answers. const claim = await fetch(`https://api.yutai.com/v1/claims/${answer.claimId}`, { headers }).then((read) => read.json()); ``` ## Try an address The paste box takes one address and asks the same read for it: the answer draws the way the example above does, and an address of a chain Yutai carries no wallet on is refused before anything is sent. The read answers live per paste, and the shared call's own cache window stands on it, so one pasted address asks the API at most once a minute. Wallet address --- # Example exchange One exchange, its stock pages: the code below is the code this page runs on its server, and the example beneath it shows the stock pages the example exchange lists, the one badge beside each stock whose company carries benefits, and why a stock wears none. ## How this is built The printed call is the one this page's read makes when it reads: it connects the SDK's client and asks the availability read for every stock the example exchange lists, one request for the whole list. The ask names the market the example asks in, the two-letter country the demo world keeps its offers in. The printed code carries a placeholder where the key stands; this page's own key never reaches it or your browser. The call the example makes ``` import { connectDistributorApi } from "@yutai/sdk-web/client"; const client = connectDistributorApi({ base: "https://api.yutai.com", key: "ytk_your-key", country: "PT" }); const answer = await client.askAvailability(["US0378331005", "US5801351017", "US6541061031", "US8552441094", "US0090661010", "US2546871060", "US67066G1040", "US88160R1014", "US5949181045", "US0231351067", "US02079K3059", "US30303M1027", "US19260Q1076"]); ``` - `GET /v1/availability` the benefits each stock the example lists carries, one batched read for the whole list - `GET /v1/eligibility` the benefits the example's own customer's wallet reaches, the read the holder's view draws - `POST /v1/claims/challenge` the words the customer's wallet signs, before the claim is asked - `POST /v1/claims` the claim, recorded on the proof the wallet signed - `GET /v1/claims/{id}` the claim's own state, in the words the read answers - `GET /v1/fulfilments` the benefits waiting on the exchange's own worker - `POST /v1/fulfilments/{id}/confirm` the exchange's worker confirming the credit with its own reference ## The example The live example is not wired yet: its key is not set, so nothing was read. ## The claim flow A benefit the example lists is one its holder can claim. The claim flow is the exchange's own: its customer's wallet signs the one message Yutai asks for, Yutai records the claim on that proof, and the benefit's credit waits on the exchange until its own worker confirms it, so the claim reads `waiting on the venue` until then and `credited by the venue` after. The flow's own code, the order the presses run it in: the two asks the SDK's client carries, then the read and the worker pass, the calls the panel above names. The flow the example runs ``` import { connectDistributorApi, refusedSentenceOf } from "@yutai/sdk-web/client"; const key = "ytk_your-key"; const client = connectDistributorApi({ base: "https://api.yutai.com", key, country: "PT" }); const headers = { Authorization: `Bearer ${key}`, Accept: "application/json" }; // The words the customer's wallet signs, before the claim is asked. const challenge = await client.askClaimChallenge(benefitId, [wallet]); const signature = await wallet.signMessage(challenge.message); // The claim, recorded on that proof. const answer = await client.askClaim({ benefitId, wallet, nonce: challenge.nonce, signature }); if (answer.status !== "claimed") throw new Error(refusedSentenceOf(answer.reason)); // The claim's own state, in the words the read answers. const claim = await fetch(`https://api.yutai.com/v1/claims/${answer.claimId}`, { headers }).then((read) => read.json()); // When the benefit waits on the exchange's own worker: the poll, then the confirm with its own reference. if (claim.state === "waiting on the venue") { const pending = await fetch(`https://api.yutai.com/v1/fulfilments`, { headers }).then((poll) => poll.json()); const row = pending.fulfilments.find((row) => row.claimId === answer.claimId); await fetch(`https://api.yutai.com/v1/fulfilments/${row.fulfilmentId}/confirm`, { method: "POST", headers: { ...headers, "Content-Type": "application/json" }, body: JSON.stringify({ venueReference: `Exchange credit for claim ${answer.claimId}` }), }); } ``` --- # Security How a distributor reviews the embed before it goes on a page: the two allowances the SDK's script asks a page's content security policy to carry, and the pin that binds the script to the exact bytes Yutai's build measured. The facts on this page come from that build, so the review works against the same script a distributor loads. ## The policy The SDK's script runs inside your page, so the page's content security policy has to allow two things: where the script loads from, and where the client the page connects reads the distributor API. The two allowances the SDK asks for: | Directive | What to allow | | --- | --- | | `script-src` | The SDK's script loads from https://yutai.com, the origin of the script line the element pages show. Add that origin to the script-src line your policy already carries. | | `connect-src` | The client your page made with `connectDistributorApi` reads Yutai's distributor API from https://api.yutai.com, the origin of the base the API reference's calls name. Pages that still connect the legacy address read the same calls from https://app.yutai.com. Add both origins to the connect-src line your policy already carries, so an address your page already connects never loses its calls. | Copy the two origins onto the lines your policy already carries, never instead of them: The two lines a distributor page adds ``` script-src https://yutai.com; connect-src https://api.yutai.com https://app.yutai.com; ``` These are the only two allowances the SDK asks a page to carry: one script origin, and the two API origins on the connect-src line. The script carries its own styles inside itself, so nothing else in the policy needs to change for the embed to run. ## The pin A policy line names an origin, not a file: any script served at that origin would load. Yutai publishes the facts its build measured over the script's exact bytes, so a distributor pins the copy it loads instead of trusting the origin alone. The script is served today. The build that published the script is version 0.1.1. The tag below pins the copy the build measured: a script at the same address that differs by one character fails the tag instead of running. The pinned script tag ``` ``` The script registers the four embed elements, each with its own page: the [credential card](/docs/credential-card), the [tier badge](/docs/tier-badge), the [benefits list](/docs/benefits-list) and the [benefits module](/docs/benefits-module). --- # Errors and limits The refusals every keyed call shares, in the interface description's own words: one row per status code, the calls that answer it, and the count each scope allows per key in one fixed minute. The one error shape a refusal carries stands on the [API reference](/docs/api#errors-heading). ## The status codes Every refusal the document's calls answer, one row per status code. Each sentence is the document's own, and the calls named under it answer with exactly that sentence. | Status | What it answers | | --- | --- | | 400 | The query could not be read. Answered by [`GET /v1/eligibility`](/docs/api/getWalletEligibility), [`GET /v1/availability`](/docs/api/getBenefitAvailability), [`GET /v1/registry/companies`](/docs/api/listRegistryCompanies), [`GET /v1/registry/instruments`](/docs/api/readRegistryInstrumentFacts). The request body could not be read. Answered by [`POST /v1/claims/challenge`](/docs/api/requestClaimChallenge), [`POST /v1/claims`](/docs/api/recordClaim), [`POST /v1/fulfilments/{id}/confirm`](/docs/api/confirmFulfilment), [`POST /v1/connect/sessions`](/docs/api/createConnectSession). | | 401 | The key is missing, malformed, unknown, revoked or expired. Answered by [`GET /v1/eligibility`](/docs/api/getWalletEligibility), [`GET /v1/availability`](/docs/api/getBenefitAvailability), [`GET /v1/registry/companies`](/docs/api/listRegistryCompanies), [`GET /v1/registry/instruments`](/docs/api/readRegistryInstrumentFacts), [`GET /v1/claims/{id}`](/docs/api/getClaim), [`POST /v1/claims/{id}/link`](/docs/api/reissueClaimLink), [`POST /v1/claims/challenge`](/docs/api/requestClaimChallenge), [`POST /v1/claims`](/docs/api/recordClaim), [`GET /v1/fulfilments`](/docs/api/listPendingFulfilments), [`POST /v1/fulfilments/{id}/confirm`](/docs/api/confirmFulfilment), [`POST /v1/connect/sessions`](/docs/api/createConnectSession), [`GET /v1/connect/sessions/{id}`](/docs/api/getConnectSession). | | 403 | The key does not carry the scope, or the wallets name chains outside the distributor's scope. The call is also refused with 403, nothing read, when the distributor is paused, suspended or closed, when the key is paused, or when the key presents from an origin it does not allow. Answered by [`GET /v1/eligibility`](/docs/api/getWalletEligibility). The key does not carry the scope. The call is also refused with 403, nothing read, when the distributor is paused, suspended or closed, when the key is paused, or when the key presents from an origin it does not allow. Answered by [`GET /v1/availability`](/docs/api/getBenefitAvailability), [`GET /v1/registry/companies`](/docs/api/listRegistryCompanies), [`GET /v1/registry/instruments`](/docs/api/readRegistryInstrumentFacts), [`GET /v1/claims/{id}`](/docs/api/getClaim), [`POST /v1/claims/{id}/link`](/docs/api/reissueClaimLink), [`GET /v1/fulfilments`](/docs/api/listPendingFulfilments), [`GET /v1/connect/sessions/{id}`](/docs/api/getConnectSession). The key does not carry the scope, or the challenge itself was refused: the venue is paused or not a self-custody venue, the benefit is not a claimable offer, or a wallet sits on a chain the venue does not serve. The call is also refused with 403, nothing read, when the distributor is paused, suspended or closed, when the key is paused, or when the key presents from an origin it does not allow. Answered by [`POST /v1/claims/challenge`](/docs/api/requestClaimChallenge). The key does not carry the scope, or the claim itself was refused: the challenge was never verified, was issued for another distributor or another benefit, or the holder's own world refused the claim, and the attested subject's statement the same - unsigned or unverifiable, expired, naming an instrument the distributor does not list or the registry no longer keeps active, naming another tenant, or for a benefit the account's holder already claimed once. The call is also refused with 403, nothing read, when the distributor is paused, suspended or closed, when the key is paused, or when the key presents from an origin it does not allow. Answered by [`POST /v1/claims`](/docs/api/recordClaim). The key does not carry the scope, or the fulfilment was already confirmed: the confirm that stands is the one that landed first. The call is also refused with 403, nothing read, when the distributor is paused, suspended or closed, when the key is paused, or when the key presents from an origin it does not allow. Answered by [`POST /v1/fulfilments/{id}/confirm`](/docs/api/confirmFulfilment). The key does not carry the scope, or the return address is not one the distributor's profile allows. The call is also refused with 403, nothing read, when the distributor is paused, suspended or closed, when the key is paused, or when the key presents from an origin it does not allow. Answered by [`POST /v1/connect/sessions`](/docs/api/createConnectSession). | | 404 | No claim answers this id under this distributor's key: an unknown id, or one another distributor's key recorded. The answer is the same for both, so no claim id can be probed. Answered by [`GET /v1/claims/{id}`](/docs/api/getClaim), [`POST /v1/claims/{id}/link`](/docs/api/reissueClaimLink). No fulfilment answers this id under this distributor's key: an unknown id, or one another distributor's key recorded. The answer is the same for both, so no fulfilment id can be probed. Answered by [`POST /v1/fulfilments/{id}/confirm`](/docs/api/confirmFulfilment). No Connect session answers this id under this distributor's key: an unknown id, a malformed id, or one another distributor's key created. The answer is the same for both, so no session id can be probed. Answered by [`GET /v1/connect/sessions/{id}`](/docs/api/getConnectSession). | | 409 | The claim the id names stands rejected or taken back, so it carries no redemption link to reissue. Answered by [`POST /v1/claims/{id}/link`](/docs/api/reissueClaimLink). | | 429 | The key made too many calls this minute, or the platform has used today's live wallet reads; the Retry-After header names the seconds until the next window opens, or until the reads open again. Nothing was read. Answered by [`GET /v1/eligibility`](/docs/api/getWalletEligibility). The key made too many calls this minute. The Retry-After header names the seconds until the next window opens. Nothing was read. Answered by [`GET /v1/availability`](/docs/api/getBenefitAvailability), [`GET /v1/registry/companies`](/docs/api/listRegistryCompanies), [`GET /v1/registry/instruments`](/docs/api/readRegistryInstrumentFacts), [`GET /v1/claims/{id}`](/docs/api/getClaim), [`POST /v1/claims/{id}/link`](/docs/api/reissueClaimLink), [`POST /v1/claims/challenge`](/docs/api/requestClaimChallenge), [`POST /v1/claims`](/docs/api/recordClaim), [`GET /v1/fulfilments`](/docs/api/listPendingFulfilments), [`POST /v1/fulfilments/{id}/confirm`](/docs/api/confirmFulfilment), [`POST /v1/connect/sessions`](/docs/api/createConnectSession), [`GET /v1/connect/sessions/{id}`](/docs/api/getConnectSession). | | 500 | The read failed inside Yutai. Nothing was changed. Answered by [`GET /v1/eligibility`](/docs/api/getWalletEligibility), [`GET /v1/availability`](/docs/api/getBenefitAvailability), [`GET /v1/registry/companies`](/docs/api/listRegistryCompanies), [`GET /v1/registry/instruments`](/docs/api/readRegistryInstrumentFacts), [`GET /v1/claims/{id}`](/docs/api/getClaim), [`POST /v1/claims/{id}/link`](/docs/api/reissueClaimLink), [`GET /v1/status`](/docs/api/getApiStatus), [`POST /v1/claims/challenge`](/docs/api/requestClaimChallenge), [`POST /v1/claims`](/docs/api/recordClaim), [`GET /v1/fulfilments`](/docs/api/listPendingFulfilments), [`POST /v1/fulfilments/{id}/confirm`](/docs/api/confirmFulfilment), [`POST /v1/connect/sessions`](/docs/api/createConnectSession), [`GET /v1/connect/sessions/{id}`](/docs/api/getConnectSession). | | 503 | The data source this read needs is not live yet, or the owners' blockchain switch does not allow the read, so the read is refused. Nothing was read. Answered by [`GET /v1/eligibility`](/docs/api/getWalletEligibility). The data source this read needs is not live yet, so the read is refused. Nothing was read. Answered by [`GET /v1/availability`](/docs/api/getBenefitAvailability), [`GET /v1/registry/companies`](/docs/api/listRegistryCompanies), [`GET /v1/registry/instruments`](/docs/api/readRegistryInstrumentFacts). | ## The limits Once the key a call presents stands, the call is counted, per key and per scope, in one fixed window of 60 seconds that starts on the minute. A call past the scope's own count is refused without being counted: the refusal answers the one error shape the Errors section carries, and its Retry-After header names the seconds to wait until the next window opens. holders.resolve: At most 60 calls per key and scope in the window. credentials.read: At most 60 calls per key and scope in the window. benefits.list: At most 60 calls per key and scope in the window. eligibility.read: At most 120 calls per key and scope in the window. availability.read: At most 600 calls per key and scope in the window. claims.write: At most 60 calls per key and scope in the window. connect.write: At most 60 calls per key and scope in the window. connect.read: At most 60 calls per key and scope in the window. How long an answer stands: Each read's answer is private to the distributor that asked and may stand for 60 seconds. A call past its scope's count answers the 429 row above: nothing was read, and the Retry-After header names when the next window opens. ## When a call is limited A limited call answers the status line, the Retry-After header and the error body below, with the interface description's own sentence for the refusal most calls share. A limited answer ``` HTTP/1.1 429 Too Many Requests Retry-After: 23 { "message": "The key made too many calls this minute. The Retry-After header names the seconds until the next window opens. Nothing was read." } ``` The loop reads that header, waits the seconds it names and tries the same call again at most three times. Wait and try again ``` async function callWithWait(url, options) { for (let attempt = 0; attempt < 3; attempt += 1) { const response = await fetch(url, options); if (response.status !== 429) return response; const seconds = Number(response.headers.get("Retry-After")); await new Promise((resolve) => setTimeout(resolve, seconds * 1000)); } throw new Error("the call was limited after three tries"); } ``` --- # Changelog This page records what changed in Yutai's distributor API, the distributor SDK and these docs since you integrated, newest first. The distributor SDK is at version 0.1.1. 2026-10-16 Distributor API: The distributor API's own address is now the one the interface document lists first, and the document's own download address moves with it. The address under the holder app stays in the document as the legacy way in: both answer the same calls, so an integration built on either keeps working. 2026-10-16 SDK: The distributor SDK, version 0.1.1, now answers at the API's own address when a page connects without naming one, names that address in its example and in the sentences its refusals carry, and keeps every address that answered before answering the same calls, so pages already integrated keep working. 2026-10-11 Distributor API: The registry companies read is callable: a distributor's key with the availability scope can now read the companies one registry holds, a page at a time, narrowing by country, ISIN or chain. See: [`GET /v1/registry/companies`](/docs/api/listRegistryCompanies). 2026-10-11 Distributor API: A distributor can now record a benefit claim for a self-custody holder: the call stands on the challenge the wallet signed, and Yutai re-checks every rule at claim time, so an earlier eligibility answer never stands for one. See: [`POST /v1/claims`](/docs/api/recordClaim). 2026-10-11 Distributor API: The claim challenge call is callable: a distributor's key with the claims.write scope can now ask for the one message a wallet signs before a claim, with the one-time nonce beside it. See: [`POST /v1/claims/challenge`](/docs/api/requestClaimChallenge). 2026-10-10 SDK: The distributor SDK is at version 0.1.1: the connect door a page calls is now connectDistributorApi and the refusal class it throws is now DistributorApiRefusal. The old names still work for one release, with one warning in the browser console that names the new door, so pages already integrated can move when they are ready. 2026-10-09 Distributor API: The planned holder-id reads were removed from the published interface: no route ever served them, and a distributor identifies a holder by wallet. The wallet eligibility read answers as before. See: [`GET /v1/eligibility`](/docs/api/getWalletEligibility). 2026-10-09 Distributor API: The eligibility read is callable: a distributor can ask which benefits one or more wallets reach. See: [`GET /v1/eligibility`](/docs/api/getWalletEligibility). 2026-10-09 SDK: The distributor SDK is at version 0.1.0: the script and one npm install both register the credential card, tier badge and benefits list elements, and the tags a distributor writes carry their types. The manifest beside the script pins the exact copy and its size. 2026-10-09 Docs: The docs gained the API reference page: every call the distributor API serves, with its parameters, its answers and a curl to copy. See: [API reference](/docs/api). 2026-10-09 Docs: The docs gained the webhooks page: what one delivery carries, when a failed delivery is tried again and how it is signed. See: [Webhooks](/docs/webhooks). 2026-10-08 Distributor API: The distributor API's first interface, v1, was released and frozen; its document described holder resolution by wallet, a holder's credential status and a holder's benefits list, none of which a route ever served, and the 9 October entry records their removal. A change that would break a distributor integrated against it waits for a new release. 2026-10-08 Docs: The docs gained the verify a credential page: check a credential a holder handed you in your own browser, with nothing sent anywhere and nothing stored. See: [Verify a credential](/docs/verify-credential). 2026-10-08 Docs: The docs gained the identifiers page: every identifier a distributor sends or reads, in one place. See: [Identifiers](/docs/identifiers). 2026-10-02 Docs: The docs opened with the credential card page: the planned embed, a live sample rendered by the element itself, and the attributes and tokens the card takes. See: [Credential card](/docs/credential-card). 2026-09-30 SDK: The distributor SDK shipped: one script tag loads it on a distributor page, and it registers the credential card element a distributor writes with plain HTML.