Skip to content
YutaiDocs
GET /v1/statusGet API key

Integrate / Claim a benefit

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.

View as .md

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.

POST/v1/claims/challenge
claims.write Callable today Ask for the challenge a wallet signs to claim a benefit
POST/v1/claims
claims.write Callable today Record a claim a wallet's own signature or a distributor's signed statement stands behind
GET/v1/claims/{id}
claims.read Callable today Read one claim this distributor recorded
POST/v1/claims/{id}/link
claims.write Callable today Reissue the redemption link of one claim this distributor recorded
GET/v1/fulfilments
claims.read Callable today List the fulfilments waiting on this distributor
POST/v1/fulfilments/{id}/confirm
claims.write Callable today Confirm 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.

POST/v1/claims/challengeThe key presents the claims.write scope.Callable today

The sample request for POST /v1/claims/challenge
{
  "benefitId": "bft_01K4V9QG000000000000000001",
  "wallets": [
    "0xabc0000000000000000000000000000000000001"
  ]
}
The sample answer for POST /v1/claims/challenge
{
  "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.

POST/v1/claimsThe key presents the claims.write scope.Callable today

The sample request for POST /v1/claims
{
  "benefitId": "bft_01K4V9QG000000000000000001",
  "wallet": "0xabc0000000000000000000000000000000000001",
  "nonce": "9f8e7d6c5b4a39281706f5e4d3c2b1a0",
  "signature": "0xababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababab"
}
The sample answer for POST /v1/claims
{
  "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.

Every reason the claim call can refuse a claim, with the sentence it answers
ReasonWhat it answers
unknown_challengeNo claim challenge stands for the nonce this claim carried. Nothing was claimed.
other_venueThe challenge was issued through another distributor. Nothing was claimed.
other_benefitThe challenge was issued for another benefit. Nothing was claimed.
not_consumedThe challenge was never signed through. Nothing was claimed.
no_holderNo wallet of the challenge carries a standing link here. Nothing was claimed.
conflicting_holdersThe challenge's wallets do not all name the same holder. Nothing was claimed.
unknown_benefitThe benefit this claim names is not one a holder can claim here. Nothing was claimed.
no_offerNo offer stands behind this benefit now. Nothing was claimed.
already_claimedThis benefit stands claimed already. Nothing was claimed.
not_eligibleThe 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.

GET/v1/claims/{id}The key presents the claims.read scope.Callable today

The sample answer for GET /v1/claims/{id}
{
  "claimId": "clm_01K4V9QG000000000000000001",
  "benefitId": "bft_01K4V9QG000000000000000001",
  "benefitName": "Sample lounge access",
  "state": "granted",
  "createdAt": "2026-10-08T12:00:00.000Z"
}

POST/v1/claims/{id}/linkThe key presents the claims.write scope.Callable today

The sample answer for POST /v1/claims/{id}/link
{
  "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.

GET/v1/fulfilmentsThe key presents the claims.read scope.Callable today

The sample answer for GET /v1/fulfilments
{
  "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"
    }
  ]
}

POST/v1/fulfilments/{id}/confirmThe key presents the claims.write scope.Callable today

The sample request for POST /v1/fulfilments/{id}/confirm
{
  "venueReference": "Lounge visit 2026-10-08, table 12"
}
The sample answer for POST /v1/fulfilments/{id}/confirm
{
  "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"
}