Claim a benefit
How a distributor claims a benefit for a holder through the distributor API's own calls, in the order they run: the challenge the wallet signs, the claim itself, the read and its link reissue, then the fulfilment list and its confirm. Every sample request and answer on this page is simulated: no real holder's data stands behind it.
The order of the calls
A claim is not one call. The six calls below run in this order: ask for the message the wallet signs, record the claim with that signature, then read the claim and reissue its link when the holder needs a fresh one. The fulfilment calls come last, once the benefit waits on your own word.
Each call links its own page, which carries every field, every bound and every refusal. This page carries the order, the key scope each call wants and one sample of each request and answer.
/v1/claims/challengeclaims.writeAsk for the challenge a wallet signs to claim a benefit/v1/claimsclaims.writeRecord a claim a wallet's own signature or a distributor's signed statement stands behind/v1/claims/{id}claims.readRead one claim this distributor recorded/v1/claims/{id}/linkclaims.writeReissue the redemption link of one claim this distributor recorded/v1/fulfilmentsclaims.readList the fulfilments waiting on this distributor/v1/fulfilments/{id}/confirmclaims.writeConfirm one fulfilment waiting on this distributor
Ask for the message to sign
Send the benefit id and the wallets that will sign. The answer carries the exact message to sign and a one-time nonce. The message names the benefit and each wallet, and the nonce stands for one claim only, so a signature cannot be moved to another claim.
The wallet signs the message as it stands. Signing moves nothing and approves nothing.
/v1/claims/challengeThe key presents the claims.write scope.
{
"benefitId": "bft_01K4V9QG000000000000000001",
"wallets": [
"0xabc0000000000000000000000000000000000001"
]
}{
"message": "Yutai claim challenge\nTenant: tnt_01K4V9QG000000000000000001\nVenue: Sample venue (tnt_01K4V9QG000000000000000002)\nBenefit: Sample lounge access (bft_01K4V9QG000000000000000001)\nWallet: 0xabc0000000000000000000000000000000000001\nNonce digest: 0xa1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4\nExpires: 2026-10-11T12:05:00.000Z\nSigning moves nothing and approves nothing.",
"nonce": "9f8e7d6c5b4a39281706f5e4d3c2b1a0",
"wallets": [
"0xabc0000000000000000000000000000000000001"
],
"expiresAt": "2026-10-11T12:05:00.000Z"
}Make the claim
Sign the message with the wallet the challenge named, then send the benefit id, that wallet, the nonce and the signature. Yutai re-checks every rule at claim time, so an earlier eligibility answer never stands for one. A granted claim answers claimed with the claim's own id; a refused one answers refused with a reason.
The exchange example runs this part as working code: it asks for the challenge, has the example wallet sign, records the claim and reads the answer back. Follow it at Example exchange; this page never copies its code.
/v1/claimsThe key presents the claims.write scope.
{
"benefitId": "bft_01K4V9QG000000000000000001",
"wallet": "0xabc0000000000000000000000000000000000001",
"nonce": "9f8e7d6c5b4a39281706f5e4d3c2b1a0",
"signature": "0xababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababab"
}{
"status": "claimed",
"claimId": "clm_01K4V9QG000000000000000001",
"benefitId": "bft_01K4V9QG000000000000000001",
"venueId": "tnt_01K4V9QG000000000000000002",
"subjectKeys": [
"evm:0xabc0000000000000000000000000000000000001"
],
"alreadyClaimed": false,
"createdAt": "2026-10-11T12:05:00.000Z",
"code": "SAMPLE-LOUNGE-0001",
"link": "https://app.yutai.com/r/clm_01K4V9QG000000000000000001?t=sample-redemption-token-0000000000000000000",
"decision": {
"eligible": true,
"tree": [
{
"kind": "tenure",
"met": true,
"note": "The wallet's own trail stands 452 days behind this ask."
}
]
}
}Refusal reasons
Every reason the claim call can refuse with, in the contract's own order. Each reason is one word a caller branches on, and the sentence beside it is the one the SDK draws for that reason.
| Reason | What it answers |
|---|---|
unknown_challenge | No claim challenge stands for the nonce this claim carried. Nothing was claimed. |
other_venue | The challenge was issued through another distributor. Nothing was claimed. |
other_benefit | The challenge was issued for another benefit. Nothing was claimed. |
not_consumed | The challenge was never signed through. Nothing was claimed. |
no_holder | No wallet of the challenge carries a standing link here. Nothing was claimed. |
conflicting_holders | The challenge's wallets do not all name the same holder. Nothing was claimed. |
unknown_benefit | The benefit this claim names is not one a holder can claim here. Nothing was claimed. |
no_offer | No offer stands behind this benefit now. Nothing was claimed. |
already_claimed | This benefit stands claimed already. Nothing was claimed. |
not_eligible | The holder does not reach this benefit's rules now. Nothing was claimed. |
The decision rides a refusal only when the world itself refused, as with not_eligible and the reasons the benefit's own rulebook, holdings trail or country give. An already_claimed refusal carries none. The errors and limits page carries every refusal status the calls answer, each in its own words.
Retry a claim
A claim is keyed by the challenge it carries, so one challenge can never mint a second claim. A retry of the same call answers the stored claim with alreadyClaimed true and the first grant's own createdAt, so the same call answers the same words twice.
A new challenge from the distributor that recorded the claim answers that claim again with alreadyClaimed true.
When another distributor holds the claim, the answer is the refusal already_claimed, with no decision beside it.
A granted answer carries the claim's own facts, and the holder's code or link when the benefit delivers one. Each field below carries the contract's own words:
alreadyClaimed- True when this call replayed a claim already recorded and false on the first grant; the same call answers the same words twice.
createdAt- The instant Yutai recorded the claim and its decision, as an ISO 8601 string.
code- The code the holder redeems, present only when the benefit delivers by code; a replay answers the same code, never a second take.
link- The holder's private redemption link, present on the first grant only; a replay carries no link.
A distributor that lost the link reissues one with the link call.
Read a claim and reissue its link
Read the claim by the id the claim answered, to see its state and its benefit in words. Reissue the link when the holder needs a fresh redemption link: only the last link issued opens the claim, so an earlier one stops working, and a rejected or taken back claim answers a refusal.
/v1/claims/{id}The key presents the claims.read scope.
{
"claimId": "clm_01K4V9QG000000000000000001",
"benefitId": "bft_01K4V9QG000000000000000001",
"benefitName": "Sample lounge access",
"state": "granted",
"createdAt": "2026-10-08T12:00:00.000Z"
}/v1/claims/{id}/linkThe key presents the claims.write scope.
{
"claimId": "clm_01K4V9QG000000000000000001",
"link": "https://app.yutai.com/r/clm_01K4V9QG000000000000000001?t=sample-redemption-token-0000000000000000000"
}A reissue that fails inside Yutai answers in its own words: The redemption link was not reissued, so the previous link still stands. Nothing was changed.
Confirm a fulfilment
List the fulfilments that wait on your own word, then confirm one with your own reference for the credit. The confirm marks the fulfilment as credited; a second confirm answers that it was already confirmed, so a retry never credits twice.
/v1/fulfilmentsThe key presents the claims.read scope.
{
"fulfilments": [
{
"fulfilmentId": "ful_01K4V9QG000000000000000001",
"claimId": "clm_01K4V9QG000000000000000001",
"benefitId": "bft_01K4V9QG000000000000000001",
"benefitName": "Sample lounge access",
"state": "waiting on the venue",
"createdAt": "2026-10-11T12:05:00.000Z"
}
]
}/v1/fulfilments/{id}/confirmThe key presents the claims.write scope.
{
"venueReference": "Lounge visit 2026-10-08, table 12"
}{
"fulfilmentId": "ful_01K4V9QG000000000000000001",
"claimId": "clm_01K4V9QG000000000000000001",
"benefitId": "bft_01K4V9QG000000000000000001",
"benefitName": "Sample lounge access",
"state": "waiting on the venue",
"createdAt": "2026-10-11T12:05:00.000Z"
}