{
  "info": {
    "_postman_id": "156aeb68-7159-4d92-b74d-89b6cce7c851",
    "name": "Verity Partner API",
    "description": "Generated from partner-openapi.yaml (1.4.0). See PARTNER_API.md for the integration guide.\n\nSet baseUrl, redeemBaseUrl and environment-specific credentials. Defaults point to staging. Production bases are https://api.theverity.xyz/api/v1/platform and https://api.theverity.xyz/api/v1/redeem. Never publish populated secrets.\n\nPrefer Checkouts: reserve using a stable checkoutKey, pay using the returned checkoutMemo, register the signature promptly, then poll. Only completed means tickets exist. The Tickets folder is the separate existing payment-first flow: wait for finalized, omit Idempotency-Key. Never send one payment through both flows.\n\nSend requests individually; this is not an unattended collection-runner payment flow. Set real shipping fields and review the quote before preparing redemption. Wallet-signed sellback uses the wallet session and requires owner approval. Wallet signing and blockchain submission happen in your wallet integration, not in these scripts.",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "auth": {
    "type": "bearer",
    "bearer": [
      {
        "key": "token",
        "value": "{{apiKey}}",
        "type": "string"
      }
    ]
  },
  "variable": [
    {
      "key": "baseUrl",
      "value": "https://api-staging.theverity.xyz/api/v1/platform",
      "type": "string",
      "description": ""
    },
    {
      "key": "redeemBaseUrl",
      "value": "https://api-staging.theverity.xyz/api/v1/redeem",
      "type": "string",
      "description": ""
    },
    {
      "key": "apiKey",
      "value": "",
      "type": "string",
      "description": "Server-side secret; never expose in browser code or exports."
    },
    {
      "key": "raffleId",
      "value": "77",
      "type": "string",
      "description": ""
    },
    {
      "key": "externalUserRef",
      "value": "usr_8812",
      "type": "string",
      "description": ""
    },
    {
      "key": "qty",
      "value": "1",
      "type": "string",
      "description": ""
    },
    {
      "key": "checkoutKey",
      "value": "",
      "type": "string",
      "description": "Set a unique purchase key once, then reuse on retries. Do not regenerate on each request."
    },
    {
      "key": "sellbackId",
      "value": "",
      "type": "string",
      "description": ""
    },
    {
      "key": "checkoutId",
      "value": "",
      "type": "string",
      "description": ""
    },
    {
      "key": "checkoutMemo",
      "value": "",
      "type": "string",
      "description": ""
    },
    {
      "key": "operationId",
      "value": "",
      "type": "string",
      "description": ""
    },
    {
      "key": "txSignature",
      "value": "",
      "type": "string",
      "description": ""
    },
    {
      "key": "walletSession",
      "value": "",
      "type": "string",
      "description": ""
    },
    {
      "key": "wallet",
      "value": "",
      "type": "string",
      "description": ""
    },
    {
      "key": "platformSlug",
      "value": "",
      "type": "string",
      "description": ""
    },
    {
      "key": "domain",
      "value": "",
      "type": "string",
      "description": ""
    },
    {
      "key": "uri",
      "value": "",
      "type": "string",
      "description": ""
    },
    {
      "key": "walletMessage",
      "value": "",
      "type": "string",
      "description": ""
    },
    {
      "key": "walletSignature",
      "value": "",
      "type": "string",
      "description": ""
    },
    {
      "key": "signedTransaction",
      "value": "",
      "type": "string",
      "description": ""
    },
    {
      "key": "redemptionId",
      "value": "",
      "type": "string",
      "description": ""
    },
    {
      "key": "productCode",
      "value": "",
      "type": "string",
      "description": ""
    },
    {
      "key": "shownShippingFeeUsd",
      "value": "",
      "type": "string",
      "description": ""
    },
    {
      "key": "recipientPhone",
      "value": "",
      "type": "string",
      "description": ""
    },
    {
      "key": "addressLine1",
      "value": "",
      "type": "string",
      "description": ""
    },
    {
      "key": "addressLine2",
      "value": "",
      "type": "string",
      "description": ""
    },
    {
      "key": "city",
      "value": "",
      "type": "string",
      "description": ""
    },
    {
      "key": "stateProvince",
      "value": "",
      "type": "string",
      "description": ""
    },
    {
      "key": "postalCode",
      "value": "",
      "type": "string",
      "description": ""
    },
    {
      "key": "countryCode",
      "value": "",
      "type": "string",
      "description": ""
    },
    {
      "key": "prizeId",
      "value": "41",
      "type": "string",
      "description": ""
    }
  ],
  "item": [
    {
      "name": "Raffles",
      "description": "Discover the raffles your platform is attached to.",
      "item": [
        {
          "name": "List your raffles",
          "request": {
            "method": "GET",
            "header": [],
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{apiKey}}",
                  "type": "string"
                }
              ]
            },
            "url": {
              "raw": "{{baseUrl}}/raffles",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "raffles"
              ]
            },
            "description": "List your raffles\n\nRaffles your platform is attached to. Raffles belonging to other platforms are\nnever visible and are indistinguishable from raffles that do not exist.\n"
          },
          "response": []
        },
        {
          "name": "Get a raffle, with payment parameters",
          "request": {
            "method": "GET",
            "header": [],
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{apiKey}}",
                  "type": "string"
                }
              ]
            },
            "url": {
              "raw": "{{baseUrl}}/raffles/{{raffleId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "raffles",
                "{{raffleId}}"
              ]
            },
            "description": "Get a raffle, with payment parameters\n\nFull raffle detail including prizes and the **`payment`** block.\n\nRead `payment` at purchase time. Never hardcode `treasury_ata`, `usdc_mint`\nor `memo_prefix`. For the checkout flow, use the checkout response's exact\n`payment.memo` instead of constructing a memo from this raffle response.\n"
          },
          "response": []
        },
        {
          "name": "A buyer's tickets and odds",
          "request": {
            "method": "GET",
            "header": [],
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{apiKey}}",
                  "type": "string"
                }
              ]
            },
            "url": {
              "raw": "{{baseUrl}}/raffles/{{raffleId}}/users/{{externalUserRef}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "raffles",
                "{{raffleId}}",
                "users",
                "{{externalUserRef}}"
              ]
            },
            "description": "A buyer's tickets and odds\n\nOne of your buyers' position in a raffle, for your own UI. Counts confirmed\npurchases made through **your** platform only; `odds_pct` is measured against\ntotal tickets sold across all platforms.\n\nAn unknown ref returns zeros rather than a 404 — you cannot probe another\nplatform's buyers with it.\n"
          },
          "response": []
        },
        {
          "name": "A buyer's prizes, with buyback and redeem state",
          "request": {
            "method": "GET",
            "header": [],
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{apiKey}}",
                  "type": "string"
                }
              ]
            },
            "url": {
              "raw": "{{baseUrl}}/users/{{externalUserRef}}/prizes",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "users",
                "{{externalUserRef}}",
                "prizes"
              ]
            },
            "description": "A buyer's prizes, with buyback and redeem state\n\nYour buyer's prize history across your raffles, newest first, including bought-back and redeemed prizes. An unknown reference returns an empty array. References are scoped to your platform. Use redeemable and redeem_blocked_reason for shipping eligibility; the wallet API checks current ownership before preparing payment. For wallet-signed sellback, authenticate the wallet and fetch GET /buybacks. These legacy buyback fields describe the API-key endpoint only. Refresh when the buyer opens their prizes or receives prize.won."
          },
          "response": []
        }
      ]
    },
    {
      "name": "Checkouts",
      "description": "Reserve tickets, submit payment and track confirmation.",
      "item": [
        {
          "name": "Recover a buyer's checkout history",
          "request": {
            "method": "GET",
            "header": [],
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{apiKey}}",
                  "type": "string"
                }
              ]
            },
            "url": {
              "raw": "{{baseUrl}}/users/{{externalUserRef}}/checkouts",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "users",
                "{{externalUserRef}}",
                "checkouts"
              ]
            },
            "description": "Recover a buyer's checkout history\n\nDurable reservation-first history for this partner's buyer, newest first. Unknown buyers return an empty list. Refresh each checkout for its latest status. This does not include payment-first ticket purchases. Never create a new payment for an unresolved checkout after browser storage is cleared; reconcile its original payment first."
          },
          "response": []
        },
        {
          "name": "Reserve tickets before collecting payment",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Idempotency-Key",
                "value": "{{checkoutKey}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{apiKey}}",
                  "type": "string"
                }
              ]
            },
            "url": {
              "raw": "{{baseUrl}}/raffles/{{raffleId}}/checkouts",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "raffles",
                "{{raffleId}}",
                "checkouts"
              ]
            },
            "description": "Reserve tickets before collecting payment\n\nSupported for both collection modes. Reuse the same Idempotency-Key on request\nretries. The response contains the immutable quote, payment_deadline and\nsettlement_deadline. Verity-collected buyers must use the exact returned memo,\nwhich includes the checkout UUID. Never initiate payment after payment_deadline.\nThe verified payer wallet is checked against the wallet limit before ticket\nconfirmation; a paid checkout exceeding that limit is refunded.\nAfter settlement expires, inventory is released while unknown payments remain\ntracked. refund_pending/refunded includes refund_operation_id, which can be polled\nat GET /payments/{operationId} or acknowledged for partner-collected refunds.\nrefund_only=true means a later successful payment will be refunded,\nnot issued tickets. Keep polling; do not start a replacement payment.\n",
            "body": {
              "mode": "raw",
              "raw": "{{requestBody}}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [],
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const template = {\"external_user_ref\":\"{{externalUserRef}}\",\"display_name\":\"Example Buyer\",\"qty\":\"{{qty}}\"};",
                  "function resolve(value, key) {",
                  "  if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).map(([k,v]) => [k, resolve(v,k)]));",
                  "  const resolved = typeof value === 'string' ? pm.variables.replaceIn(value) : value;",
                  "  if (['qty','prizeId','shownShippingFeeUsd'].includes(key)) {",
                  "    if (resolved === '' || !Number.isFinite(Number(resolved))) throw new Error('Set a numeric ' + key + ' before sending.');",
                  "    return Number(resolved);",
                  "  }",
                  "  return resolved;",
                  "}",
                  "pm.variables.set('requestBody', JSON.stringify(resolve(template)));"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const body = pm.response.json();",
                  "if (body.success) { pm.collectionVariables.set('checkoutId', body.data.id); pm.collectionVariables.set('operationId', body.data.id); pm.collectionVariables.set('checkoutMemo', body.data.payment?.memo ?? ''); }"
                ]
              }
            }
          ]
        },
        {
          "name": "Read checkout status without sending money",
          "request": {
            "method": "GET",
            "header": [],
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{apiKey}}",
                  "type": "string"
                }
              ]
            },
            "url": {
              "raw": "{{baseUrl}}/raffles/{{raffleId}}/checkouts/{{checkoutId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "raffles",
                "{{raffleId}}",
                "checkouts",
                "{{checkoutId}}"
              ]
            },
            "description": "Read checkout status without sending money"
          },
          "response": []
        },
        {
          "name": "Register a payment for asynchronous verification",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{apiKey}}",
                  "type": "string"
                }
              ]
            },
            "url": {
              "raw": "{{baseUrl}}/raffles/{{raffleId}}/checkouts/{{checkoutId}}/payment",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "raffles",
                "{{raffleId}}",
                "checkouts",
                "{{checkoutId}}",
                "payment"
              ]
            },
            "description": "Register a payment for asynchronous verification\n\nVerity-collected: send tx_signature as soon as the transaction is prepared/submitted,\nbefore payment_deadline; do not wait for finalization. Repeat with the same signature\nafter timeouts. A different signature is accepted only after the previous attempt is\nproven invalid/failed and before cutoff. A late first receipt needs finalized block-time\nevidence of payment before cutoff; otherwise the verified payment is refund-only.\nPartner-collected: send an empty body to attest that this checkout was collected once;\nlate attestations create a partner refund obligation. Never charge again on HTTP retries.\n",
            "body": {
              "mode": "raw",
              "raw": "{{requestBody}}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [],
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const template = {\"tx_signature\":\"{{txSignature}}\"};",
                  "function resolve(value, key) {",
                  "  if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).map(([k,v]) => [k, resolve(v,k)]));",
                  "  const resolved = typeof value === 'string' ? pm.variables.replaceIn(value) : value;",
                  "  if (['qty','prizeId','shownShippingFeeUsd'].includes(key)) {",
                  "    if (resolved === '' || !Number.isFinite(Number(resolved))) throw new Error('Set a numeric ' + key + ' before sending.');",
                  "    return Number(resolved);",
                  "  }",
                  "  return resolved;",
                  "}",
                  "pm.variables.set('requestBody', JSON.stringify(resolve(template)));"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "Payments",
      "description": "Track purchase and refund outcomes.",
      "item": [
        {
          "name": "Read your payment or refund obligation",
          "request": {
            "method": "GET",
            "header": [],
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{apiKey}}",
                  "type": "string"
                }
              ]
            },
            "url": {
              "raw": "{{baseUrl}}/payments/{{operationId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "payments",
                "{{operationId}}"
              ]
            },
            "description": "Read your payment or refund obligation"
          },
          "response": []
        },
        {
          "name": "Confirm a partner-collected refund was paid by your system",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{apiKey}}",
                  "type": "string"
                }
              ]
            },
            "url": {
              "raw": "{{baseUrl}}/payments/{{operationId}}/refund-confirmation",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "payments",
                "{{operationId}}",
                "refund-confirmation"
              ]
            },
            "description": "Confirm a partner-collected refund was paid by your system\n\nOnly for partner-collected refund obligations. Call after your own idempotent refund\nsucceeds. Repeating the same acknowledgement is safe. Never use this for Verity\ntreasury refunds; Verity verifies those on-chain itself.\n",
            "body": {
              "mode": "raw",
              "raw": "{{requestBody}}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [],
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const template = {};",
                  "function resolve(value, key) {",
                  "  if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).map(([k,v]) => [k, resolve(v,k)]));",
                  "  const resolved = typeof value === 'string' ? pm.variables.replaceIn(value) : value;",
                  "  if (['qty','prizeId','shownShippingFeeUsd'].includes(key)) {",
                  "    if (resolved === '' || !Number.isFinite(Number(resolved))) throw new Error('Set a numeric ' + key + ' before sending.');",
                  "    return Number(resolved);",
                  "  }",
                  "  return resolved;",
                  "}",
                  "pm.variables.set('requestBody', JSON.stringify(resolve(template)));"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "Tickets",
      "description": "The money path. Issue tickets against a settled USDC payment.",
      "item": [
        {
          "name": "Issue tickets against a settled USDC payment",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{apiKey}}",
                  "type": "string"
                }
              ]
            },
            "url": {
              "raw": "{{baseUrl}}/raffles/{{raffleId}}/tickets",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "raffles",
                "{{raffleId}}",
                "tickets"
              ]
            },
            "description": "Issue tickets against a settled USDC payment\n\n**Existing payment-first flow.** Submit the signature of a finalized USDC transfer and receive\nticket codes.\n\n### Building the payment\n\n```js\nimport { createTransferCheckedInstruction, getAssociatedTokenAddressSync } from \"@solana/spl-token\";\nimport { PublicKey, Transaction, TransactionInstruction, sendAndConfirmTransaction } from \"@solana/web3.js\";\n\nconst MEMO_PROGRAM_ID = new PublicKey(\"MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr\");\n\n// Public payment details fetched by your server. Never expose your partner API key.\nconst { treasury_ata, usdc_mint, decimals, memo_prefix } = detail.data.payment;\n\n// 2. Exact amount, fixed-point. Floats will drift you off by base units.\nfunction decimalToBaseUnits(value, decimals = 6) {\n  const match = /^(\\d+)(?:\\.(\\d+))?$/.exec(value);\n  if (!match || !Number.isInteger(decimals) || decimals < 0 || decimals > 9) {\n    throw new Error(\"Invalid token amount\");\n  }\n  const fraction = match[2] ?? \"\";\n  if (fraction.length > decimals && /[1-9]/.test(fraction.slice(decimals))) {\n    throw new Error(\"Amount exceeds token precision\");\n  }\n  return BigInt(match[1]) * 10n ** BigInt(decimals)\n    + BigInt(fraction.slice(0, decimals).padEnd(decimals, \"0\") || \"0\");\n}\n\nconst baseUnits = decimalToBaseUnits(detail.data.ticket_price_usd, decimals) * BigInt(qty);\n\n// 3. transferChecked (NOT transfer) + exactly one memo.\nconst mint = new PublicKey(usdc_mint);\nconst tx = new Transaction().add(\n  createTransferCheckedInstruction(\n    getAssociatedTokenAddressSync(mint, payer.publicKey),\n    mint,\n    new PublicKey(treasury_ata),\n    payer.publicKey,\n    baseUnits,\n    decimals,\n  ),\n  new TransactionInstruction({\n    keys: [],\n    programId: MEMO_PROGRAM_ID,\n    data: Buffer.from(`${memo_prefix}:${externalUserRef}`, \"utf8\"),\n  }),\n);\n\n// 4. Wait for FINALIZED before submitting to Verity.\nconst signature = await sendAndConfirmTransaction(connection, tx, [payer], {\n  commitment: \"finalized\",\n});\n```\n\n### Timing\n\nVerity verifies the transaction at **`finalized`** commitment. Network confirmation\ncan take time; a client timeout does not prove the purchase failed.\n\n- Wait for `finalized` yourself before calling this endpoint.\n- Set your HTTP timeout to **at least 30 seconds**. This is not a fast call.\n- `409 TX_NOT_FOUND` means \"not visible yet, retry\", not \"invalid\".\n\n### Two `200` outcomes\n\n`replayed: false` — tickets issued now. `replayed: true` — this signature was\nalready issued and you are receiving the original result. Both are successes.\n\nRetry only with the same signature, raffle, quantity and buyer. A changed purchase identity returns IDEMPOTENCY_KEY_REUSED. Do not send this payment through the checkout API as well.\n",
            "body": {
              "mode": "raw",
              "raw": "{{requestBody}}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "200 — Issued now",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "auth": {
                  "type": "bearer",
                  "bearer": [
                    {
                      "key": "token",
                      "value": "{{apiKey}}",
                      "type": "string"
                    }
                  ]
                },
                "url": {
                  "raw": "{{baseUrl}}/raffles/{{raffleId}}/tickets",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "raffles",
                    "{{raffleId}}",
                    "tickets"
                  ]
                },
                "description": "Issue tickets against a settled USDC payment\n\n**Existing payment-first flow.** Submit the signature of a finalized USDC transfer and receive\nticket codes.\n\n### Building the payment\n\n```js\nimport { createTransferCheckedInstruction, getAssociatedTokenAddressSync } from \"@solana/spl-token\";\nimport { PublicKey, Transaction, TransactionInstruction, sendAndConfirmTransaction } from \"@solana/web3.js\";\n\nconst MEMO_PROGRAM_ID = new PublicKey(\"MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr\");\n\n// Public payment details fetched by your server. Never expose your partner API key.\nconst { treasury_ata, usdc_mint, decimals, memo_prefix } = detail.data.payment;\n\n// 2. Exact amount, fixed-point. Floats will drift you off by base units.\nfunction decimalToBaseUnits(value, decimals = 6) {\n  const match = /^(\\d+)(?:\\.(\\d+))?$/.exec(value);\n  if (!match || !Number.isInteger(decimals) || decimals < 0 || decimals > 9) {\n    throw new Error(\"Invalid token amount\");\n  }\n  const fraction = match[2] ?? \"\";\n  if (fraction.length > decimals && /[1-9]/.test(fraction.slice(decimals))) {\n    throw new Error(\"Amount exceeds token precision\");\n  }\n  return BigInt(match[1]) * 10n ** BigInt(decimals)\n    + BigInt(fraction.slice(0, decimals).padEnd(decimals, \"0\") || \"0\");\n}\n\nconst baseUnits = decimalToBaseUnits(detail.data.ticket_price_usd, decimals) * BigInt(qty);\n\n// 3. transferChecked (NOT transfer) + exactly one memo.\nconst mint = new PublicKey(usdc_mint);\nconst tx = new Transaction().add(\n  createTransferCheckedInstruction(\n    getAssociatedTokenAddressSync(mint, payer.publicKey),\n    mint,\n    new PublicKey(treasury_ata),\n    payer.publicKey,\n    baseUnits,\n    decimals,\n  ),\n  new TransactionInstruction({\n    keys: [],\n    programId: MEMO_PROGRAM_ID,\n    data: Buffer.from(`${memo_prefix}:${externalUserRef}`, \"utf8\"),\n  }),\n);\n\n// 4. Wait for FINALIZED before submitting to Verity.\nconst signature = await sendAndConfirmTransaction(connection, tx, [payer], {\n  commitment: \"finalized\",\n});\n```\n\n### Timing\n\nVerity verifies the transaction at **`finalized`** commitment. Network confirmation\ncan take time; a client timeout does not prove the purchase failed.\n\n- Wait for `finalized` yourself before calling this endpoint.\n- Set your HTTP timeout to **at least 30 seconds**. This is not a fast call.\n- `409 TX_NOT_FOUND` means \"not visible yet, retry\", not \"invalid\".\n\n### Two `200` outcomes\n\n`replayed: false` — tickets issued now. `replayed: true` — this signature was\nalready issued and you are receiving the original result. Both are successes.\n\nRetry only with the same signature, raffle, quantity and buyer. A changed purchase identity returns IDEMPOTENCY_KEY_REUSED. Do not send this payment through the checkout API as well.\n",
                "body": {
                  "mode": "raw",
                  "raw": "{{requestBody}}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "code": 200,
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"data\": {\n    \"purchase_id\": 9134,\n    \"raffle_id\": 77,\n    \"qty\": 5,\n    \"total_usd\": 250,\n    \"tickets\": [\n      \"R077-T00410\",\n      \"R077-T00411\",\n      \"R077-T00412\",\n      \"R077-T00413\",\n      \"R077-T00414\"\n    ],\n    \"replayed\": false\n  }\n}",
              "_postman_previewlanguage": "json"
            },
            {
              "name": "200 — Retry of a request that already succeeded — also a success",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "auth": {
                  "type": "bearer",
                  "bearer": [
                    {
                      "key": "token",
                      "value": "{{apiKey}}",
                      "type": "string"
                    }
                  ]
                },
                "url": {
                  "raw": "{{baseUrl}}/raffles/{{raffleId}}/tickets",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "raffles",
                    "{{raffleId}}",
                    "tickets"
                  ]
                },
                "description": "Issue tickets against a settled USDC payment\n\n**Existing payment-first flow.** Submit the signature of a finalized USDC transfer and receive\nticket codes.\n\n### Building the payment\n\n```js\nimport { createTransferCheckedInstruction, getAssociatedTokenAddressSync } from \"@solana/spl-token\";\nimport { PublicKey, Transaction, TransactionInstruction, sendAndConfirmTransaction } from \"@solana/web3.js\";\n\nconst MEMO_PROGRAM_ID = new PublicKey(\"MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr\");\n\n// Public payment details fetched by your server. Never expose your partner API key.\nconst { treasury_ata, usdc_mint, decimals, memo_prefix } = detail.data.payment;\n\n// 2. Exact amount, fixed-point. Floats will drift you off by base units.\nfunction decimalToBaseUnits(value, decimals = 6) {\n  const match = /^(\\d+)(?:\\.(\\d+))?$/.exec(value);\n  if (!match || !Number.isInteger(decimals) || decimals < 0 || decimals > 9) {\n    throw new Error(\"Invalid token amount\");\n  }\n  const fraction = match[2] ?? \"\";\n  if (fraction.length > decimals && /[1-9]/.test(fraction.slice(decimals))) {\n    throw new Error(\"Amount exceeds token precision\");\n  }\n  return BigInt(match[1]) * 10n ** BigInt(decimals)\n    + BigInt(fraction.slice(0, decimals).padEnd(decimals, \"0\") || \"0\");\n}\n\nconst baseUnits = decimalToBaseUnits(detail.data.ticket_price_usd, decimals) * BigInt(qty);\n\n// 3. transferChecked (NOT transfer) + exactly one memo.\nconst mint = new PublicKey(usdc_mint);\nconst tx = new Transaction().add(\n  createTransferCheckedInstruction(\n    getAssociatedTokenAddressSync(mint, payer.publicKey),\n    mint,\n    new PublicKey(treasury_ata),\n    payer.publicKey,\n    baseUnits,\n    decimals,\n  ),\n  new TransactionInstruction({\n    keys: [],\n    programId: MEMO_PROGRAM_ID,\n    data: Buffer.from(`${memo_prefix}:${externalUserRef}`, \"utf8\"),\n  }),\n);\n\n// 4. Wait for FINALIZED before submitting to Verity.\nconst signature = await sendAndConfirmTransaction(connection, tx, [payer], {\n  commitment: \"finalized\",\n});\n```\n\n### Timing\n\nVerity verifies the transaction at **`finalized`** commitment. Network confirmation\ncan take time; a client timeout does not prove the purchase failed.\n\n- Wait for `finalized` yourself before calling this endpoint.\n- Set your HTTP timeout to **at least 30 seconds**. This is not a fast call.\n- `409 TX_NOT_FOUND` means \"not visible yet, retry\", not \"invalid\".\n\n### Two `200` outcomes\n\n`replayed: false` — tickets issued now. `replayed: true` — this signature was\nalready issued and you are receiving the original result. Both are successes.\n\nRetry only with the same signature, raffle, quantity and buyer. A changed purchase identity returns IDEMPOTENCY_KEY_REUSED. Do not send this payment through the checkout API as well.\n",
                "body": {
                  "mode": "raw",
                  "raw": "{{requestBody}}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "code": 200,
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": true,\n  \"data\": {\n    \"purchase_id\": 9134,\n    \"raffle_id\": 77,\n    \"qty\": 5,\n    \"total_usd\": 250,\n    \"tickets\": [\n      \"R077-T00410\",\n      \"R077-T00411\",\n      \"R077-T00412\",\n      \"R077-T00413\",\n      \"R077-T00414\"\n    ],\n    \"replayed\": true\n  }\n}",
              "_postman_previewlanguage": "json"
            },
            {
              "name": "400 — Used `transfer` instead of `transferChecked` — refunded",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "auth": {
                  "type": "bearer",
                  "bearer": [
                    {
                      "key": "token",
                      "value": "{{apiKey}}",
                      "type": "string"
                    }
                  ]
                },
                "url": {
                  "raw": "{{baseUrl}}/raffles/{{raffleId}}/tickets",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "raffles",
                    "{{raffleId}}",
                    "tickets"
                  ]
                },
                "description": "Issue tickets against a settled USDC payment\n\n**Existing payment-first flow.** Submit the signature of a finalized USDC transfer and receive\nticket codes.\n\n### Building the payment\n\n```js\nimport { createTransferCheckedInstruction, getAssociatedTokenAddressSync } from \"@solana/spl-token\";\nimport { PublicKey, Transaction, TransactionInstruction, sendAndConfirmTransaction } from \"@solana/web3.js\";\n\nconst MEMO_PROGRAM_ID = new PublicKey(\"MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr\");\n\n// Public payment details fetched by your server. Never expose your partner API key.\nconst { treasury_ata, usdc_mint, decimals, memo_prefix } = detail.data.payment;\n\n// 2. Exact amount, fixed-point. Floats will drift you off by base units.\nfunction decimalToBaseUnits(value, decimals = 6) {\n  const match = /^(\\d+)(?:\\.(\\d+))?$/.exec(value);\n  if (!match || !Number.isInteger(decimals) || decimals < 0 || decimals > 9) {\n    throw new Error(\"Invalid token amount\");\n  }\n  const fraction = match[2] ?? \"\";\n  if (fraction.length > decimals && /[1-9]/.test(fraction.slice(decimals))) {\n    throw new Error(\"Amount exceeds token precision\");\n  }\n  return BigInt(match[1]) * 10n ** BigInt(decimals)\n    + BigInt(fraction.slice(0, decimals).padEnd(decimals, \"0\") || \"0\");\n}\n\nconst baseUnits = decimalToBaseUnits(detail.data.ticket_price_usd, decimals) * BigInt(qty);\n\n// 3. transferChecked (NOT transfer) + exactly one memo.\nconst mint = new PublicKey(usdc_mint);\nconst tx = new Transaction().add(\n  createTransferCheckedInstruction(\n    getAssociatedTokenAddressSync(mint, payer.publicKey),\n    mint,\n    new PublicKey(treasury_ata),\n    payer.publicKey,\n    baseUnits,\n    decimals,\n  ),\n  new TransactionInstruction({\n    keys: [],\n    programId: MEMO_PROGRAM_ID,\n    data: Buffer.from(`${memo_prefix}:${externalUserRef}`, \"utf8\"),\n  }),\n);\n\n// 4. Wait for FINALIZED before submitting to Verity.\nconst signature = await sendAndConfirmTransaction(connection, tx, [payer], {\n  commitment: \"finalized\",\n});\n```\n\n### Timing\n\nVerity verifies the transaction at **`finalized`** commitment. Network confirmation\ncan take time; a client timeout does not prove the purchase failed.\n\n- Wait for `finalized` yourself before calling this endpoint.\n- Set your HTTP timeout to **at least 30 seconds**. This is not a fast call.\n- `409 TX_NOT_FOUND` means \"not visible yet, retry\", not \"invalid\".\n\n### Two `200` outcomes\n\n`replayed: false` — tickets issued now. `replayed: true` — this signature was\nalready issued and you are receiving the original result. Both are successes.\n\nRetry only with the same signature, raffle, quantity and buyer. A changed purchase identity returns IDEMPOTENCY_KEY_REUSED. Do not send this payment through the checkout API as well.\n",
                "body": {
                  "mode": "raw",
                  "raw": "{{requestBody}}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "code": 400,
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": false,\n  \"error\": \"TX_USED_UNCHECKED_TRANSFER\",\n  \"refund_status\": \"in_flight\",\n  \"refund_tx_signature\": null\n}",
              "_postman_previewlanguage": "json"
            },
            {
              "name": "400 — Memo missing, duplicated, or wrong — NOT refunded",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "auth": {
                  "type": "bearer",
                  "bearer": [
                    {
                      "key": "token",
                      "value": "{{apiKey}}",
                      "type": "string"
                    }
                  ]
                },
                "url": {
                  "raw": "{{baseUrl}}/raffles/{{raffleId}}/tickets",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "raffles",
                    "{{raffleId}}",
                    "tickets"
                  ]
                },
                "description": "Issue tickets against a settled USDC payment\n\n**Existing payment-first flow.** Submit the signature of a finalized USDC transfer and receive\nticket codes.\n\n### Building the payment\n\n```js\nimport { createTransferCheckedInstruction, getAssociatedTokenAddressSync } from \"@solana/spl-token\";\nimport { PublicKey, Transaction, TransactionInstruction, sendAndConfirmTransaction } from \"@solana/web3.js\";\n\nconst MEMO_PROGRAM_ID = new PublicKey(\"MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr\");\n\n// Public payment details fetched by your server. Never expose your partner API key.\nconst { treasury_ata, usdc_mint, decimals, memo_prefix } = detail.data.payment;\n\n// 2. Exact amount, fixed-point. Floats will drift you off by base units.\nfunction decimalToBaseUnits(value, decimals = 6) {\n  const match = /^(\\d+)(?:\\.(\\d+))?$/.exec(value);\n  if (!match || !Number.isInteger(decimals) || decimals < 0 || decimals > 9) {\n    throw new Error(\"Invalid token amount\");\n  }\n  const fraction = match[2] ?? \"\";\n  if (fraction.length > decimals && /[1-9]/.test(fraction.slice(decimals))) {\n    throw new Error(\"Amount exceeds token precision\");\n  }\n  return BigInt(match[1]) * 10n ** BigInt(decimals)\n    + BigInt(fraction.slice(0, decimals).padEnd(decimals, \"0\") || \"0\");\n}\n\nconst baseUnits = decimalToBaseUnits(detail.data.ticket_price_usd, decimals) * BigInt(qty);\n\n// 3. transferChecked (NOT transfer) + exactly one memo.\nconst mint = new PublicKey(usdc_mint);\nconst tx = new Transaction().add(\n  createTransferCheckedInstruction(\n    getAssociatedTokenAddressSync(mint, payer.publicKey),\n    mint,\n    new PublicKey(treasury_ata),\n    payer.publicKey,\n    baseUnits,\n    decimals,\n  ),\n  new TransactionInstruction({\n    keys: [],\n    programId: MEMO_PROGRAM_ID,\n    data: Buffer.from(`${memo_prefix}:${externalUserRef}`, \"utf8\"),\n  }),\n);\n\n// 4. Wait for FINALIZED before submitting to Verity.\nconst signature = await sendAndConfirmTransaction(connection, tx, [payer], {\n  commitment: \"finalized\",\n});\n```\n\n### Timing\n\nVerity verifies the transaction at **`finalized`** commitment. Network confirmation\ncan take time; a client timeout does not prove the purchase failed.\n\n- Wait for `finalized` yourself before calling this endpoint.\n- Set your HTTP timeout to **at least 30 seconds**. This is not a fast call.\n- `409 TX_NOT_FOUND` means \"not visible yet, retry\", not \"invalid\".\n\n### Two `200` outcomes\n\n`replayed: false` — tickets issued now. `replayed: true` — this signature was\nalready issued and you are receiving the original result. Both are successes.\n\nRetry only with the same signature, raffle, quantity and buyer. A changed purchase identity returns IDEMPOTENCY_KEY_REUSED. Do not send this payment through the checkout API as well.\n",
                "body": {
                  "mode": "raw",
                  "raw": "{{requestBody}}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "code": 400,
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": false,\n  \"error\": \"TX_MEMO_MISMATCH\"\n}",
              "_postman_previewlanguage": "json"
            },
            {
              "name": "400 — Sent an Idempotency-Key header",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "auth": {
                  "type": "bearer",
                  "bearer": [
                    {
                      "key": "token",
                      "value": "{{apiKey}}",
                      "type": "string"
                    }
                  ]
                },
                "url": {
                  "raw": "{{baseUrl}}/raffles/{{raffleId}}/tickets",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "raffles",
                    "{{raffleId}}",
                    "tickets"
                  ]
                },
                "description": "Issue tickets against a settled USDC payment\n\n**Existing payment-first flow.** Submit the signature of a finalized USDC transfer and receive\nticket codes.\n\n### Building the payment\n\n```js\nimport { createTransferCheckedInstruction, getAssociatedTokenAddressSync } from \"@solana/spl-token\";\nimport { PublicKey, Transaction, TransactionInstruction, sendAndConfirmTransaction } from \"@solana/web3.js\";\n\nconst MEMO_PROGRAM_ID = new PublicKey(\"MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr\");\n\n// Public payment details fetched by your server. Never expose your partner API key.\nconst { treasury_ata, usdc_mint, decimals, memo_prefix } = detail.data.payment;\n\n// 2. Exact amount, fixed-point. Floats will drift you off by base units.\nfunction decimalToBaseUnits(value, decimals = 6) {\n  const match = /^(\\d+)(?:\\.(\\d+))?$/.exec(value);\n  if (!match || !Number.isInteger(decimals) || decimals < 0 || decimals > 9) {\n    throw new Error(\"Invalid token amount\");\n  }\n  const fraction = match[2] ?? \"\";\n  if (fraction.length > decimals && /[1-9]/.test(fraction.slice(decimals))) {\n    throw new Error(\"Amount exceeds token precision\");\n  }\n  return BigInt(match[1]) * 10n ** BigInt(decimals)\n    + BigInt(fraction.slice(0, decimals).padEnd(decimals, \"0\") || \"0\");\n}\n\nconst baseUnits = decimalToBaseUnits(detail.data.ticket_price_usd, decimals) * BigInt(qty);\n\n// 3. transferChecked (NOT transfer) + exactly one memo.\nconst mint = new PublicKey(usdc_mint);\nconst tx = new Transaction().add(\n  createTransferCheckedInstruction(\n    getAssociatedTokenAddressSync(mint, payer.publicKey),\n    mint,\n    new PublicKey(treasury_ata),\n    payer.publicKey,\n    baseUnits,\n    decimals,\n  ),\n  new TransactionInstruction({\n    keys: [],\n    programId: MEMO_PROGRAM_ID,\n    data: Buffer.from(`${memo_prefix}:${externalUserRef}`, \"utf8\"),\n  }),\n);\n\n// 4. Wait for FINALIZED before submitting to Verity.\nconst signature = await sendAndConfirmTransaction(connection, tx, [payer], {\n  commitment: \"finalized\",\n});\n```\n\n### Timing\n\nVerity verifies the transaction at **`finalized`** commitment. Network confirmation\ncan take time; a client timeout does not prove the purchase failed.\n\n- Wait for `finalized` yourself before calling this endpoint.\n- Set your HTTP timeout to **at least 30 seconds**. This is not a fast call.\n- `409 TX_NOT_FOUND` means \"not visible yet, retry\", not \"invalid\".\n\n### Two `200` outcomes\n\n`replayed: false` — tickets issued now. `replayed: true` — this signature was\nalready issued and you are receiving the original result. Both are successes.\n\nRetry only with the same signature, raffle, quantity and buyer. A changed purchase identity returns IDEMPOTENCY_KEY_REUSED. Do not send this payment through the checkout API as well.\n",
                "body": {
                  "mode": "raw",
                  "raw": "{{requestBody}}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "code": 400,
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": false,\n  \"error\": \"TX_SIGNATURE_NOT_ALLOWED\"\n}",
              "_postman_previewlanguage": "json"
            },
            {
              "name": "400 — Bad body",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "auth": {
                  "type": "bearer",
                  "bearer": [
                    {
                      "key": "token",
                      "value": "{{apiKey}}",
                      "type": "string"
                    }
                  ]
                },
                "url": {
                  "raw": "{{baseUrl}}/raffles/{{raffleId}}/tickets",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "raffles",
                    "{{raffleId}}",
                    "tickets"
                  ]
                },
                "description": "Issue tickets against a settled USDC payment\n\n**Existing payment-first flow.** Submit the signature of a finalized USDC transfer and receive\nticket codes.\n\n### Building the payment\n\n```js\nimport { createTransferCheckedInstruction, getAssociatedTokenAddressSync } from \"@solana/spl-token\";\nimport { PublicKey, Transaction, TransactionInstruction, sendAndConfirmTransaction } from \"@solana/web3.js\";\n\nconst MEMO_PROGRAM_ID = new PublicKey(\"MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr\");\n\n// Public payment details fetched by your server. Never expose your partner API key.\nconst { treasury_ata, usdc_mint, decimals, memo_prefix } = detail.data.payment;\n\n// 2. Exact amount, fixed-point. Floats will drift you off by base units.\nfunction decimalToBaseUnits(value, decimals = 6) {\n  const match = /^(\\d+)(?:\\.(\\d+))?$/.exec(value);\n  if (!match || !Number.isInteger(decimals) || decimals < 0 || decimals > 9) {\n    throw new Error(\"Invalid token amount\");\n  }\n  const fraction = match[2] ?? \"\";\n  if (fraction.length > decimals && /[1-9]/.test(fraction.slice(decimals))) {\n    throw new Error(\"Amount exceeds token precision\");\n  }\n  return BigInt(match[1]) * 10n ** BigInt(decimals)\n    + BigInt(fraction.slice(0, decimals).padEnd(decimals, \"0\") || \"0\");\n}\n\nconst baseUnits = decimalToBaseUnits(detail.data.ticket_price_usd, decimals) * BigInt(qty);\n\n// 3. transferChecked (NOT transfer) + exactly one memo.\nconst mint = new PublicKey(usdc_mint);\nconst tx = new Transaction().add(\n  createTransferCheckedInstruction(\n    getAssociatedTokenAddressSync(mint, payer.publicKey),\n    mint,\n    new PublicKey(treasury_ata),\n    payer.publicKey,\n    baseUnits,\n    decimals,\n  ),\n  new TransactionInstruction({\n    keys: [],\n    programId: MEMO_PROGRAM_ID,\n    data: Buffer.from(`${memo_prefix}:${externalUserRef}`, \"utf8\"),\n  }),\n);\n\n// 4. Wait for FINALIZED before submitting to Verity.\nconst signature = await sendAndConfirmTransaction(connection, tx, [payer], {\n  commitment: \"finalized\",\n});\n```\n\n### Timing\n\nVerity verifies the transaction at **`finalized`** commitment. Network confirmation\ncan take time; a client timeout does not prove the purchase failed.\n\n- Wait for `finalized` yourself before calling this endpoint.\n- Set your HTTP timeout to **at least 30 seconds**. This is not a fast call.\n- `409 TX_NOT_FOUND` means \"not visible yet, retry\", not \"invalid\".\n\n### Two `200` outcomes\n\n`replayed: false` — tickets issued now. `replayed: true` — this signature was\nalready issued and you are receiving the original result. Both are successes.\n\nRetry only with the same signature, raffle, quantity and buyer. A changed purchase identity returns IDEMPOTENCY_KEY_REUSED. Do not send this payment through the checkout API as well.\n",
                "body": {
                  "mode": "raw",
                  "raw": "{{requestBody}}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "code": 400,
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": false,\n  \"error\": \"VALIDATION_ERROR\",\n  \"details\": [\n    {\n      \"code\": \"too_small\",\n      \"path\": [\n        \"qty\"\n      ],\n      \"message\": \"Number must be greater than 0\"\n    }\n  ]\n}",
              "_postman_previewlanguage": "json"
            },
            {
              "name": "409 — Not finalized yet — retry the same signature",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "auth": {
                  "type": "bearer",
                  "bearer": [
                    {
                      "key": "token",
                      "value": "{{apiKey}}",
                      "type": "string"
                    }
                  ]
                },
                "url": {
                  "raw": "{{baseUrl}}/raffles/{{raffleId}}/tickets",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "raffles",
                    "{{raffleId}}",
                    "tickets"
                  ]
                },
                "description": "Issue tickets against a settled USDC payment\n\n**Existing payment-first flow.** Submit the signature of a finalized USDC transfer and receive\nticket codes.\n\n### Building the payment\n\n```js\nimport { createTransferCheckedInstruction, getAssociatedTokenAddressSync } from \"@solana/spl-token\";\nimport { PublicKey, Transaction, TransactionInstruction, sendAndConfirmTransaction } from \"@solana/web3.js\";\n\nconst MEMO_PROGRAM_ID = new PublicKey(\"MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr\");\n\n// Public payment details fetched by your server. Never expose your partner API key.\nconst { treasury_ata, usdc_mint, decimals, memo_prefix } = detail.data.payment;\n\n// 2. Exact amount, fixed-point. Floats will drift you off by base units.\nfunction decimalToBaseUnits(value, decimals = 6) {\n  const match = /^(\\d+)(?:\\.(\\d+))?$/.exec(value);\n  if (!match || !Number.isInteger(decimals) || decimals < 0 || decimals > 9) {\n    throw new Error(\"Invalid token amount\");\n  }\n  const fraction = match[2] ?? \"\";\n  if (fraction.length > decimals && /[1-9]/.test(fraction.slice(decimals))) {\n    throw new Error(\"Amount exceeds token precision\");\n  }\n  return BigInt(match[1]) * 10n ** BigInt(decimals)\n    + BigInt(fraction.slice(0, decimals).padEnd(decimals, \"0\") || \"0\");\n}\n\nconst baseUnits = decimalToBaseUnits(detail.data.ticket_price_usd, decimals) * BigInt(qty);\n\n// 3. transferChecked (NOT transfer) + exactly one memo.\nconst mint = new PublicKey(usdc_mint);\nconst tx = new Transaction().add(\n  createTransferCheckedInstruction(\n    getAssociatedTokenAddressSync(mint, payer.publicKey),\n    mint,\n    new PublicKey(treasury_ata),\n    payer.publicKey,\n    baseUnits,\n    decimals,\n  ),\n  new TransactionInstruction({\n    keys: [],\n    programId: MEMO_PROGRAM_ID,\n    data: Buffer.from(`${memo_prefix}:${externalUserRef}`, \"utf8\"),\n  }),\n);\n\n// 4. Wait for FINALIZED before submitting to Verity.\nconst signature = await sendAndConfirmTransaction(connection, tx, [payer], {\n  commitment: \"finalized\",\n});\n```\n\n### Timing\n\nVerity verifies the transaction at **`finalized`** commitment. Network confirmation\ncan take time; a client timeout does not prove the purchase failed.\n\n- Wait for `finalized` yourself before calling this endpoint.\n- Set your HTTP timeout to **at least 30 seconds**. This is not a fast call.\n- `409 TX_NOT_FOUND` means \"not visible yet, retry\", not \"invalid\".\n\n### Two `200` outcomes\n\n`replayed: false` — tickets issued now. `replayed: true` — this signature was\nalready issued and you are receiving the original result. Both are successes.\n\nRetry only with the same signature, raffle, quantity and buyer. A changed purchase identity returns IDEMPOTENCY_KEY_REUSED. Do not send this payment through the checkout API as well.\n",
                "body": {
                  "mode": "raw",
                  "raw": "{{requestBody}}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "code": 409,
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": false,\n  \"error\": \"TX_NOT_FOUND\"\n}",
              "_postman_previewlanguage": "json"
            },
            {
              "name": "409 — Wrong amount (including overpayment) — refunded in full",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "auth": {
                  "type": "bearer",
                  "bearer": [
                    {
                      "key": "token",
                      "value": "{{apiKey}}",
                      "type": "string"
                    }
                  ]
                },
                "url": {
                  "raw": "{{baseUrl}}/raffles/{{raffleId}}/tickets",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "raffles",
                    "{{raffleId}}",
                    "tickets"
                  ]
                },
                "description": "Issue tickets against a settled USDC payment\n\n**Existing payment-first flow.** Submit the signature of a finalized USDC transfer and receive\nticket codes.\n\n### Building the payment\n\n```js\nimport { createTransferCheckedInstruction, getAssociatedTokenAddressSync } from \"@solana/spl-token\";\nimport { PublicKey, Transaction, TransactionInstruction, sendAndConfirmTransaction } from \"@solana/web3.js\";\n\nconst MEMO_PROGRAM_ID = new PublicKey(\"MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr\");\n\n// Public payment details fetched by your server. Never expose your partner API key.\nconst { treasury_ata, usdc_mint, decimals, memo_prefix } = detail.data.payment;\n\n// 2. Exact amount, fixed-point. Floats will drift you off by base units.\nfunction decimalToBaseUnits(value, decimals = 6) {\n  const match = /^(\\d+)(?:\\.(\\d+))?$/.exec(value);\n  if (!match || !Number.isInteger(decimals) || decimals < 0 || decimals > 9) {\n    throw new Error(\"Invalid token amount\");\n  }\n  const fraction = match[2] ?? \"\";\n  if (fraction.length > decimals && /[1-9]/.test(fraction.slice(decimals))) {\n    throw new Error(\"Amount exceeds token precision\");\n  }\n  return BigInt(match[1]) * 10n ** BigInt(decimals)\n    + BigInt(fraction.slice(0, decimals).padEnd(decimals, \"0\") || \"0\");\n}\n\nconst baseUnits = decimalToBaseUnits(detail.data.ticket_price_usd, decimals) * BigInt(qty);\n\n// 3. transferChecked (NOT transfer) + exactly one memo.\nconst mint = new PublicKey(usdc_mint);\nconst tx = new Transaction().add(\n  createTransferCheckedInstruction(\n    getAssociatedTokenAddressSync(mint, payer.publicKey),\n    mint,\n    new PublicKey(treasury_ata),\n    payer.publicKey,\n    baseUnits,\n    decimals,\n  ),\n  new TransactionInstruction({\n    keys: [],\n    programId: MEMO_PROGRAM_ID,\n    data: Buffer.from(`${memo_prefix}:${externalUserRef}`, \"utf8\"),\n  }),\n);\n\n// 4. Wait for FINALIZED before submitting to Verity.\nconst signature = await sendAndConfirmTransaction(connection, tx, [payer], {\n  commitment: \"finalized\",\n});\n```\n\n### Timing\n\nVerity verifies the transaction at **`finalized`** commitment. Network confirmation\ncan take time; a client timeout does not prove the purchase failed.\n\n- Wait for `finalized` yourself before calling this endpoint.\n- Set your HTTP timeout to **at least 30 seconds**. This is not a fast call.\n- `409 TX_NOT_FOUND` means \"not visible yet, retry\", not \"invalid\".\n\n### Two `200` outcomes\n\n`replayed: false` — tickets issued now. `replayed: true` — this signature was\nalready issued and you are receiving the original result. Both are successes.\n\nRetry only with the same signature, raffle, quantity and buyer. A changed purchase identity returns IDEMPOTENCY_KEY_REUSED. Do not send this payment through the checkout API as well.\n",
                "body": {
                  "mode": "raw",
                  "raw": "{{requestBody}}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "code": 409,
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": false,\n  \"error\": \"TX_AMOUNT_MISMATCH\",\n  \"refund_status\": \"in_flight\",\n  \"refund_tx_signature\": null\n}",
              "_postman_previewlanguage": "json"
            },
            {
              "name": "409 — Paid, but the raffle filled mid-checkout — refund queued",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "auth": {
                  "type": "bearer",
                  "bearer": [
                    {
                      "key": "token",
                      "value": "{{apiKey}}",
                      "type": "string"
                    }
                  ]
                },
                "url": {
                  "raw": "{{baseUrl}}/raffles/{{raffleId}}/tickets",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "raffles",
                    "{{raffleId}}",
                    "tickets"
                  ]
                },
                "description": "Issue tickets against a settled USDC payment\n\n**Existing payment-first flow.** Submit the signature of a finalized USDC transfer and receive\nticket codes.\n\n### Building the payment\n\n```js\nimport { createTransferCheckedInstruction, getAssociatedTokenAddressSync } from \"@solana/spl-token\";\nimport { PublicKey, Transaction, TransactionInstruction, sendAndConfirmTransaction } from \"@solana/web3.js\";\n\nconst MEMO_PROGRAM_ID = new PublicKey(\"MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr\");\n\n// Public payment details fetched by your server. Never expose your partner API key.\nconst { treasury_ata, usdc_mint, decimals, memo_prefix } = detail.data.payment;\n\n// 2. Exact amount, fixed-point. Floats will drift you off by base units.\nfunction decimalToBaseUnits(value, decimals = 6) {\n  const match = /^(\\d+)(?:\\.(\\d+))?$/.exec(value);\n  if (!match || !Number.isInteger(decimals) || decimals < 0 || decimals > 9) {\n    throw new Error(\"Invalid token amount\");\n  }\n  const fraction = match[2] ?? \"\";\n  if (fraction.length > decimals && /[1-9]/.test(fraction.slice(decimals))) {\n    throw new Error(\"Amount exceeds token precision\");\n  }\n  return BigInt(match[1]) * 10n ** BigInt(decimals)\n    + BigInt(fraction.slice(0, decimals).padEnd(decimals, \"0\") || \"0\");\n}\n\nconst baseUnits = decimalToBaseUnits(detail.data.ticket_price_usd, decimals) * BigInt(qty);\n\n// 3. transferChecked (NOT transfer) + exactly one memo.\nconst mint = new PublicKey(usdc_mint);\nconst tx = new Transaction().add(\n  createTransferCheckedInstruction(\n    getAssociatedTokenAddressSync(mint, payer.publicKey),\n    mint,\n    new PublicKey(treasury_ata),\n    payer.publicKey,\n    baseUnits,\n    decimals,\n  ),\n  new TransactionInstruction({\n    keys: [],\n    programId: MEMO_PROGRAM_ID,\n    data: Buffer.from(`${memo_prefix}:${externalUserRef}`, \"utf8\"),\n  }),\n);\n\n// 4. Wait for FINALIZED before submitting to Verity.\nconst signature = await sendAndConfirmTransaction(connection, tx, [payer], {\n  commitment: \"finalized\",\n});\n```\n\n### Timing\n\nVerity verifies the transaction at **`finalized`** commitment. Network confirmation\ncan take time; a client timeout does not prove the purchase failed.\n\n- Wait for `finalized` yourself before calling this endpoint.\n- Set your HTTP timeout to **at least 30 seconds**. This is not a fast call.\n- `409 TX_NOT_FOUND` means \"not visible yet, retry\", not \"invalid\".\n\n### Two `200` outcomes\n\n`replayed: false` — tickets issued now. `replayed: true` — this signature was\nalready issued and you are receiving the original result. Both are successes.\n\nRetry only with the same signature, raffle, quantity and buyer. A changed purchase identity returns IDEMPOTENCY_KEY_REUSED. Do not send this payment through the checkout API as well.\n",
                "body": {
                  "mode": "raw",
                  "raw": "{{requestBody}}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "code": 409,
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": false,\n  \"error\": \"RAFFLE_MAX_ENTRIES_EXCEEDED\",\n  \"refund_status\": \"in_flight\",\n  \"refund_tx_signature\": null\n}",
              "_postman_previewlanguage": "json"
            },
            {
              "name": "409 — A previous attempt on this signature was already refunded",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "auth": {
                  "type": "bearer",
                  "bearer": [
                    {
                      "key": "token",
                      "value": "{{apiKey}}",
                      "type": "string"
                    }
                  ]
                },
                "url": {
                  "raw": "{{baseUrl}}/raffles/{{raffleId}}/tickets",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "raffles",
                    "{{raffleId}}",
                    "tickets"
                  ]
                },
                "description": "Issue tickets against a settled USDC payment\n\n**Existing payment-first flow.** Submit the signature of a finalized USDC transfer and receive\nticket codes.\n\n### Building the payment\n\n```js\nimport { createTransferCheckedInstruction, getAssociatedTokenAddressSync } from \"@solana/spl-token\";\nimport { PublicKey, Transaction, TransactionInstruction, sendAndConfirmTransaction } from \"@solana/web3.js\";\n\nconst MEMO_PROGRAM_ID = new PublicKey(\"MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr\");\n\n// Public payment details fetched by your server. Never expose your partner API key.\nconst { treasury_ata, usdc_mint, decimals, memo_prefix } = detail.data.payment;\n\n// 2. Exact amount, fixed-point. Floats will drift you off by base units.\nfunction decimalToBaseUnits(value, decimals = 6) {\n  const match = /^(\\d+)(?:\\.(\\d+))?$/.exec(value);\n  if (!match || !Number.isInteger(decimals) || decimals < 0 || decimals > 9) {\n    throw new Error(\"Invalid token amount\");\n  }\n  const fraction = match[2] ?? \"\";\n  if (fraction.length > decimals && /[1-9]/.test(fraction.slice(decimals))) {\n    throw new Error(\"Amount exceeds token precision\");\n  }\n  return BigInt(match[1]) * 10n ** BigInt(decimals)\n    + BigInt(fraction.slice(0, decimals).padEnd(decimals, \"0\") || \"0\");\n}\n\nconst baseUnits = decimalToBaseUnits(detail.data.ticket_price_usd, decimals) * BigInt(qty);\n\n// 3. transferChecked (NOT transfer) + exactly one memo.\nconst mint = new PublicKey(usdc_mint);\nconst tx = new Transaction().add(\n  createTransferCheckedInstruction(\n    getAssociatedTokenAddressSync(mint, payer.publicKey),\n    mint,\n    new PublicKey(treasury_ata),\n    payer.publicKey,\n    baseUnits,\n    decimals,\n  ),\n  new TransactionInstruction({\n    keys: [],\n    programId: MEMO_PROGRAM_ID,\n    data: Buffer.from(`${memo_prefix}:${externalUserRef}`, \"utf8\"),\n  }),\n);\n\n// 4. Wait for FINALIZED before submitting to Verity.\nconst signature = await sendAndConfirmTransaction(connection, tx, [payer], {\n  commitment: \"finalized\",\n});\n```\n\n### Timing\n\nVerity verifies the transaction at **`finalized`** commitment. Network confirmation\ncan take time; a client timeout does not prove the purchase failed.\n\n- Wait for `finalized` yourself before calling this endpoint.\n- Set your HTTP timeout to **at least 30 seconds**. This is not a fast call.\n- `409 TX_NOT_FOUND` means \"not visible yet, retry\", not \"invalid\".\n\n### Two `200` outcomes\n\n`replayed: false` — tickets issued now. `replayed: true` — this signature was\nalready issued and you are receiving the original result. Both are successes.\n\nRetry only with the same signature, raffle, quantity and buyer. A changed purchase identity returns IDEMPOTENCY_KEY_REUSED. Do not send this payment through the checkout API as well.\n",
                "body": {
                  "mode": "raw",
                  "raw": "{{requestBody}}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "code": 409,
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": false,\n  \"error\": \"PAYMENT_REFUNDED\",\n  \"reason\": \"RAFFLE_MAX_ENTRIES_EXCEEDED\",\n  \"refund_status\": \"paid\",\n  \"refund_tx_signature\": \"2VbQ8sT1mL9pK2wY7nH4gF6xJ5cR3dN8vB6aE9tU4yW7\"\n}",
              "_postman_previewlanguage": "json"
            },
            {
              "name": "409 — Re-posted before the refund completed — keep polling, never pay again",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "auth": {
                  "type": "bearer",
                  "bearer": [
                    {
                      "key": "token",
                      "value": "{{apiKey}}",
                      "type": "string"
                    }
                  ]
                },
                "url": {
                  "raw": "{{baseUrl}}/raffles/{{raffleId}}/tickets",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "raffles",
                    "{{raffleId}}",
                    "tickets"
                  ]
                },
                "description": "Issue tickets against a settled USDC payment\n\n**Existing payment-first flow.** Submit the signature of a finalized USDC transfer and receive\nticket codes.\n\n### Building the payment\n\n```js\nimport { createTransferCheckedInstruction, getAssociatedTokenAddressSync } from \"@solana/spl-token\";\nimport { PublicKey, Transaction, TransactionInstruction, sendAndConfirmTransaction } from \"@solana/web3.js\";\n\nconst MEMO_PROGRAM_ID = new PublicKey(\"MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr\");\n\n// Public payment details fetched by your server. Never expose your partner API key.\nconst { treasury_ata, usdc_mint, decimals, memo_prefix } = detail.data.payment;\n\n// 2. Exact amount, fixed-point. Floats will drift you off by base units.\nfunction decimalToBaseUnits(value, decimals = 6) {\n  const match = /^(\\d+)(?:\\.(\\d+))?$/.exec(value);\n  if (!match || !Number.isInteger(decimals) || decimals < 0 || decimals > 9) {\n    throw new Error(\"Invalid token amount\");\n  }\n  const fraction = match[2] ?? \"\";\n  if (fraction.length > decimals && /[1-9]/.test(fraction.slice(decimals))) {\n    throw new Error(\"Amount exceeds token precision\");\n  }\n  return BigInt(match[1]) * 10n ** BigInt(decimals)\n    + BigInt(fraction.slice(0, decimals).padEnd(decimals, \"0\") || \"0\");\n}\n\nconst baseUnits = decimalToBaseUnits(detail.data.ticket_price_usd, decimals) * BigInt(qty);\n\n// 3. transferChecked (NOT transfer) + exactly one memo.\nconst mint = new PublicKey(usdc_mint);\nconst tx = new Transaction().add(\n  createTransferCheckedInstruction(\n    getAssociatedTokenAddressSync(mint, payer.publicKey),\n    mint,\n    new PublicKey(treasury_ata),\n    payer.publicKey,\n    baseUnits,\n    decimals,\n  ),\n  new TransactionInstruction({\n    keys: [],\n    programId: MEMO_PROGRAM_ID,\n    data: Buffer.from(`${memo_prefix}:${externalUserRef}`, \"utf8\"),\n  }),\n);\n\n// 4. Wait for FINALIZED before submitting to Verity.\nconst signature = await sendAndConfirmTransaction(connection, tx, [payer], {\n  commitment: \"finalized\",\n});\n```\n\n### Timing\n\nVerity verifies the transaction at **`finalized`** commitment. Network confirmation\ncan take time; a client timeout does not prove the purchase failed.\n\n- Wait for `finalized` yourself before calling this endpoint.\n- Set your HTTP timeout to **at least 30 seconds**. This is not a fast call.\n- `409 TX_NOT_FOUND` means \"not visible yet, retry\", not \"invalid\".\n\n### Two `200` outcomes\n\n`replayed: false` — tickets issued now. `replayed: true` — this signature was\nalready issued and you are receiving the original result. Both are successes.\n\nRetry only with the same signature, raffle, quantity and buyer. A changed purchase identity returns IDEMPOTENCY_KEY_REUSED. Do not send this payment through the checkout API as well.\n",
                "body": {
                  "mode": "raw",
                  "raw": "{{requestBody}}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "code": 409,
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": false,\n  \"error\": \"PAYMENT_REFUND_IN_FLIGHT\",\n  \"reason\": \"RAFFLE_MAX_ENTRIES_EXCEEDED\",\n  \"refund_status\": \"in_flight\",\n  \"refund_tx_signature\": null\n}",
              "_postman_previewlanguage": "json"
            },
            {
              "name": "409 — This signature already bought tickets on another raffle",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "auth": {
                  "type": "bearer",
                  "bearer": [
                    {
                      "key": "token",
                      "value": "{{apiKey}}",
                      "type": "string"
                    }
                  ]
                },
                "url": {
                  "raw": "{{baseUrl}}/raffles/{{raffleId}}/tickets",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "raffles",
                    "{{raffleId}}",
                    "tickets"
                  ]
                },
                "description": "Issue tickets against a settled USDC payment\n\n**Existing payment-first flow.** Submit the signature of a finalized USDC transfer and receive\nticket codes.\n\n### Building the payment\n\n```js\nimport { createTransferCheckedInstruction, getAssociatedTokenAddressSync } from \"@solana/spl-token\";\nimport { PublicKey, Transaction, TransactionInstruction, sendAndConfirmTransaction } from \"@solana/web3.js\";\n\nconst MEMO_PROGRAM_ID = new PublicKey(\"MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr\");\n\n// Public payment details fetched by your server. Never expose your partner API key.\nconst { treasury_ata, usdc_mint, decimals, memo_prefix } = detail.data.payment;\n\n// 2. Exact amount, fixed-point. Floats will drift you off by base units.\nfunction decimalToBaseUnits(value, decimals = 6) {\n  const match = /^(\\d+)(?:\\.(\\d+))?$/.exec(value);\n  if (!match || !Number.isInteger(decimals) || decimals < 0 || decimals > 9) {\n    throw new Error(\"Invalid token amount\");\n  }\n  const fraction = match[2] ?? \"\";\n  if (fraction.length > decimals && /[1-9]/.test(fraction.slice(decimals))) {\n    throw new Error(\"Amount exceeds token precision\");\n  }\n  return BigInt(match[1]) * 10n ** BigInt(decimals)\n    + BigInt(fraction.slice(0, decimals).padEnd(decimals, \"0\") || \"0\");\n}\n\nconst baseUnits = decimalToBaseUnits(detail.data.ticket_price_usd, decimals) * BigInt(qty);\n\n// 3. transferChecked (NOT transfer) + exactly one memo.\nconst mint = new PublicKey(usdc_mint);\nconst tx = new Transaction().add(\n  createTransferCheckedInstruction(\n    getAssociatedTokenAddressSync(mint, payer.publicKey),\n    mint,\n    new PublicKey(treasury_ata),\n    payer.publicKey,\n    baseUnits,\n    decimals,\n  ),\n  new TransactionInstruction({\n    keys: [],\n    programId: MEMO_PROGRAM_ID,\n    data: Buffer.from(`${memo_prefix}:${externalUserRef}`, \"utf8\"),\n  }),\n);\n\n// 4. Wait for FINALIZED before submitting to Verity.\nconst signature = await sendAndConfirmTransaction(connection, tx, [payer], {\n  commitment: \"finalized\",\n});\n```\n\n### Timing\n\nVerity verifies the transaction at **`finalized`** commitment. Network confirmation\ncan take time; a client timeout does not prove the purchase failed.\n\n- Wait for `finalized` yourself before calling this endpoint.\n- Set your HTTP timeout to **at least 30 seconds**. This is not a fast call.\n- `409 TX_NOT_FOUND` means \"not visible yet, retry\", not \"invalid\".\n\n### Two `200` outcomes\n\n`replayed: false` — tickets issued now. `replayed: true` — this signature was\nalready issued and you are receiving the original result. Both are successes.\n\nRetry only with the same signature, raffle, quantity and buyer. A changed purchase identity returns IDEMPOTENCY_KEY_REUSED. Do not send this payment through the checkout API as well.\n",
                "body": {
                  "mode": "raw",
                  "raw": "{{requestBody}}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "code": 409,
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": false,\n  \"error\": \"IDEMPOTENCY_KEY_REUSED\"\n}",
              "_postman_previewlanguage": "json"
            }
          ],
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const template = {\"external_user_ref\":\"{{externalUserRef}}\",\"qty\":\"{{qty}}\",\"tx_signature\":\"{{txSignature}}\"};",
                  "function resolve(value, key) {",
                  "  if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).map(([k,v]) => [k, resolve(v,k)]));",
                  "  const resolved = typeof value === 'string' ? pm.variables.replaceIn(value) : value;",
                  "  if (['qty','prizeId','shownShippingFeeUsd'].includes(key)) {",
                  "    if (resolved === '' || !Number.isFinite(Number(resolved))) throw new Error('Set a numeric ' + key + ' before sending.');",
                  "    return Number(resolved);",
                  "  }",
                  "  return resolved;",
                  "}",
                  "pm.variables.set('requestBody', JSON.stringify(resolve(template)));"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "Buyback",
      "description": "Currently unavailable for partner winners. Respect the returned availability flags.",
      "item": [
        {
          "name": "Compatibility buyback (API key cannot sign for a wallet)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{apiKey}}",
                  "type": "string"
                }
              ]
            },
            "url": {
              "raw": "{{baseUrl}}/raffles/{{raffleId}}/prizes/{{prizeId}}/buyback",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "raffles",
                "{{raffleId}}",
                "prizes",
                "{{prizeId}}",
                "buyback"
              ]
            },
            "description": "Compatibility buyback (API key cannot sign for a wallet)\n\nRetained for existing integrations. Use the wallet-signed sellback endpoints for new integrations; this API-key endpoint cannot sign for the holder.\nHide the action when `prize.won`\nreports `buyback.available: false` or the prize response reports\n`buyback.eligible: false`. An amount or deadline alone does not enable buyback.\nAn otherwise eligible request returns `409 ITEM_WALLET_SIGNATURE_REQUIRED`.\nProcessing, unavailable, expired or shipping-related errors may be returned first.\nOffer redemption when the prize response says it is redeemable.\n\nExisting clients can use the response formats below to recognize previous\nrequests. They should not offer new buybacks through this endpoint.\n",
            "body": {
              "mode": "raw",
              "raw": "{{requestBody}}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "403 — That ref did not win that prize",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "auth": {
                  "type": "bearer",
                  "bearer": [
                    {
                      "key": "token",
                      "value": "{{apiKey}}",
                      "type": "string"
                    }
                  ]
                },
                "url": {
                  "raw": "{{baseUrl}}/raffles/{{raffleId}}/prizes/{{prizeId}}/buyback",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "raffles",
                    "{{raffleId}}",
                    "prizes",
                    "{{prizeId}}",
                    "buyback"
                  ]
                },
                "description": "Compatibility buyback (API key cannot sign for a wallet)\n\nRetained for existing integrations. Use the wallet-signed sellback endpoints for new integrations; this API-key endpoint cannot sign for the holder.\nHide the action when `prize.won`\nreports `buyback.available: false` or the prize response reports\n`buyback.eligible: false`. An amount or deadline alone does not enable buyback.\nAn otherwise eligible request returns `409 ITEM_WALLET_SIGNATURE_REQUIRED`.\nProcessing, unavailable, expired or shipping-related errors may be returned first.\nOffer redemption when the prize response says it is redeemable.\n\nExisting clients can use the response formats below to recognize previous\nrequests. They should not offer new buybacks through this endpoint.\n",
                "body": {
                  "mode": "raw",
                  "raw": "{{requestBody}}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "code": 403,
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": false,\n  \"error\": \"RAFFLE_NOT_WINNER\"\n}",
              "_postman_previewlanguage": "json"
            },
            {
              "name": "403 — Your platform is not verity_collected",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "auth": {
                  "type": "bearer",
                  "bearer": [
                    {
                      "key": "token",
                      "value": "{{apiKey}}",
                      "type": "string"
                    }
                  ]
                },
                "url": {
                  "raw": "{{baseUrl}}/raffles/{{raffleId}}/prizes/{{prizeId}}/buyback",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "raffles",
                    "{{raffleId}}",
                    "prizes",
                    "{{prizeId}}",
                    "buyback"
                  ]
                },
                "description": "Compatibility buyback (API key cannot sign for a wallet)\n\nRetained for existing integrations. Use the wallet-signed sellback endpoints for new integrations; this API-key endpoint cannot sign for the holder.\nHide the action when `prize.won`\nreports `buyback.available: false` or the prize response reports\n`buyback.eligible: false`. An amount or deadline alone does not enable buyback.\nAn otherwise eligible request returns `409 ITEM_WALLET_SIGNATURE_REQUIRED`.\nProcessing, unavailable, expired or shipping-related errors may be returned first.\nOffer redemption when the prize response says it is redeemable.\n\nExisting clients can use the response formats below to recognize previous\nrequests. They should not offer new buybacks through this endpoint.\n",
                "body": {
                  "mode": "raw",
                  "raw": "{{requestBody}}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "code": 403,
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": false,\n  \"error\": \"RAFFLE_BUYBACK_UNAVAILABLE\"\n}",
              "_postman_previewlanguage": "json"
            },
            {
              "name": "403 — The prize token belongs to another buyer",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "auth": {
                  "type": "bearer",
                  "bearer": [
                    {
                      "key": "token",
                      "value": "{{apiKey}}",
                      "type": "string"
                    }
                  ]
                },
                "url": {
                  "raw": "{{baseUrl}}/raffles/{{raffleId}}/prizes/{{prizeId}}/buyback",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "raffles",
                    "{{raffleId}}",
                    "prizes",
                    "{{prizeId}}",
                    "buyback"
                  ]
                },
                "description": "Compatibility buyback (API key cannot sign for a wallet)\n\nRetained for existing integrations. Use the wallet-signed sellback endpoints for new integrations; this API-key endpoint cannot sign for the holder.\nHide the action when `prize.won`\nreports `buyback.available: false` or the prize response reports\n`buyback.eligible: false`. An amount or deadline alone does not enable buyback.\nAn otherwise eligible request returns `409 ITEM_WALLET_SIGNATURE_REQUIRED`.\nProcessing, unavailable, expired or shipping-related errors may be returned first.\nOffer redemption when the prize response says it is redeemable.\n\nExisting clients can use the response formats below to recognize previous\nrequests. They should not offer new buybacks through this endpoint.\n",
                "body": {
                  "mode": "raw",
                  "raw": "{{requestBody}}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "code": 403,
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": false,\n  \"error\": \"ITEM_NOT_OWNED\"\n}",
              "_postman_previewlanguage": "json"
            },
            {
              "name": "409 — Partner winner holding the token in their own wallet",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "auth": {
                  "type": "bearer",
                  "bearer": [
                    {
                      "key": "token",
                      "value": "{{apiKey}}",
                      "type": "string"
                    }
                  ]
                },
                "url": {
                  "raw": "{{baseUrl}}/raffles/{{raffleId}}/prizes/{{prizeId}}/buyback",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "raffles",
                    "{{raffleId}}",
                    "prizes",
                    "{{prizeId}}",
                    "buyback"
                  ]
                },
                "description": "Compatibility buyback (API key cannot sign for a wallet)\n\nRetained for existing integrations. Use the wallet-signed sellback endpoints for new integrations; this API-key endpoint cannot sign for the holder.\nHide the action when `prize.won`\nreports `buyback.available: false` or the prize response reports\n`buyback.eligible: false`. An amount or deadline alone does not enable buyback.\nAn otherwise eligible request returns `409 ITEM_WALLET_SIGNATURE_REQUIRED`.\nProcessing, unavailable, expired or shipping-related errors may be returned first.\nOffer redemption when the prize response says it is redeemable.\n\nExisting clients can use the response formats below to recognize previous\nrequests. They should not offer new buybacks through this endpoint.\n",
                "body": {
                  "mode": "raw",
                  "raw": "{{requestBody}}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "code": 409,
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": false,\n  \"error\": \"ITEM_WALLET_SIGNATURE_REQUIRED\"\n}",
              "_postman_previewlanguage": "json"
            },
            {
              "name": "409 — expired",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "auth": {
                  "type": "bearer",
                  "bearer": [
                    {
                      "key": "token",
                      "value": "{{apiKey}}",
                      "type": "string"
                    }
                  ]
                },
                "url": {
                  "raw": "{{baseUrl}}/raffles/{{raffleId}}/prizes/{{prizeId}}/buyback",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "raffles",
                    "{{raffleId}}",
                    "prizes",
                    "{{prizeId}}",
                    "buyback"
                  ]
                },
                "description": "Compatibility buyback (API key cannot sign for a wallet)\n\nRetained for existing integrations. Use the wallet-signed sellback endpoints for new integrations; this API-key endpoint cannot sign for the holder.\nHide the action when `prize.won`\nreports `buyback.available: false` or the prize response reports\n`buyback.eligible: false`. An amount or deadline alone does not enable buyback.\nAn otherwise eligible request returns `409 ITEM_WALLET_SIGNATURE_REQUIRED`.\nProcessing, unavailable, expired or shipping-related errors may be returned first.\nOffer redemption when the prize response says it is redeemable.\n\nExisting clients can use the response formats below to recognize previous\nrequests. They should not offer new buybacks through this endpoint.\n",
                "body": {
                  "mode": "raw",
                  "raw": "{{requestBody}}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "code": 409,
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": false,\n  \"error\": \"ITEM_BUYBACK_EXPIRED\"\n}",
              "_postman_previewlanguage": "json"
            },
            {
              "name": "409 — award_processing",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "auth": {
                  "type": "bearer",
                  "bearer": [
                    {
                      "key": "token",
                      "value": "{{apiKey}}",
                      "type": "string"
                    }
                  ]
                },
                "url": {
                  "raw": "{{baseUrl}}/raffles/{{raffleId}}/prizes/{{prizeId}}/buyback",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "raffles",
                    "{{raffleId}}",
                    "prizes",
                    "{{prizeId}}",
                    "buyback"
                  ]
                },
                "description": "Compatibility buyback (API key cannot sign for a wallet)\n\nRetained for existing integrations. Use the wallet-signed sellback endpoints for new integrations; this API-key endpoint cannot sign for the holder.\nHide the action when `prize.won`\nreports `buyback.available: false` or the prize response reports\n`buyback.eligible: false`. An amount or deadline alone does not enable buyback.\nAn otherwise eligible request returns `409 ITEM_WALLET_SIGNATURE_REQUIRED`.\nProcessing, unavailable, expired or shipping-related errors may be returned first.\nOffer redemption when the prize response says it is redeemable.\n\nExisting clients can use the response formats below to recognize previous\nrequests. They should not offer new buybacks through this endpoint.\n",
                "body": {
                  "mode": "raw",
                  "raw": "{{requestBody}}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "code": 409,
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"success\": false,\n  \"error\": \"ITEM_AWARD_PROCESSING\"\n}",
              "_postman_previewlanguage": "json"
            }
          ],
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const template = {\"external_user_ref\":\"{{externalUserRef}}\"};",
                  "function resolve(value, key) {",
                  "  if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).map(([k,v]) => [k, resolve(v,k)]));",
                  "  const resolved = typeof value === 'string' ? pm.variables.replaceIn(value) : value;",
                  "  if (['qty','prizeId','shownShippingFeeUsd'].includes(key)) {",
                  "    if (resolved === '' || !Number.isFinite(Number(resolved))) throw new Error('Set a numeric ' + key + ' before sending.');",
                  "    return Number(resolved);",
                  "  }",
                  "  return resolved;",
                  "}",
                  "pm.variables.set('requestBody', JSON.stringify(resolve(template)));"
                ]
              }
            }
          ]
        },
        {
          "name": "Sellback offers for the signed-in wallet",
          "request": {
            "method": "GET",
            "header": [],
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{walletSession}}",
                  "type": "string"
                }
              ]
            },
            "url": {
              "raw": "{{redeemBaseUrl}}/buybacks",
              "host": [
                "{{redeemBaseUrl}}"
              ],
              "path": [
                "buybacks"
              ]
            },
            "description": "Sellback offers for the signed-in wallet\n\nUse available and expires_at to enable sellback. Resume operation_id when awaiting_signature or processing. The prepare endpoint rechecks ownership and eligibility."
          },
          "response": []
        },
        {
          "name": "Prepare a wallet-signed sellback",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{walletSession}}",
                  "type": "string"
                }
              ]
            },
            "url": {
              "raw": "{{redeemBaseUrl}}/prizes/{{prizeId}}/sellback/prepare",
              "host": [
                "{{redeemBaseUrl}}"
              ],
              "path": [
                "prizes",
                "{{prizeId}}",
                "sellback",
                "prepare"
              ]
            },
            "description": "Prepare a wallet-signed sellback\n\nReturns the same active operation on retry. The NFT return and payout are atomic. Sign the returned transaction unchanged; do not broadcast it from the browser. Verity pays the network fee.",
            "body": {
              "mode": "raw",
              "raw": "{{requestBody}}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [],
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const template = {};",
                  "function resolve(value, key) {",
                  "  if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).map(([k,v]) => [k, resolve(v,k)]));",
                  "  const resolved = typeof value === 'string' ? pm.variables.replaceIn(value) : value;",
                  "  if (['qty','prizeId','shownShippingFeeUsd'].includes(key)) {",
                  "    if (resolved === '' || !Number.isFinite(Number(resolved))) throw new Error('Set a numeric ' + key + ' before sending.');",
                  "    return Number(resolved);",
                  "  }",
                  "  return resolved;",
                  "}",
                  "pm.variables.set('requestBody', JSON.stringify(resolve(template)));"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const body = pm.response.json();",
                  "if (body.success) pm.collectionVariables.set('sellbackId', body.data.id);"
                ]
              }
            }
          ]
        },
        {
          "name": "Check a sellback",
          "request": {
            "method": "GET",
            "header": [],
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{walletSession}}",
                  "type": "string"
                }
              ]
            },
            "url": {
              "raw": "{{redeemBaseUrl}}/sellbacks/{{sellbackId}}",
              "host": [
                "{{redeemBaseUrl}}"
              ],
              "path": [
                "sellbacks",
                "{{sellbackId}}"
              ]
            },
            "description": "Check a sellback"
          },
          "response": []
        },
        {
          "name": "Submit the owner-signed sellback",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{walletSession}}",
                  "type": "string"
                }
              ]
            },
            "url": {
              "raw": "{{redeemBaseUrl}}/sellbacks/{{sellbackId}}/submit",
              "host": [
                "{{redeemBaseUrl}}"
              ],
              "path": [
                "sellbacks",
                "{{sellbackId}}",
                "submit"
              ]
            },
            "description": "Submit the owner-signed sellback\n\nSave signed bytes before sending. Retries retain the original accepted transaction. After timeout, poll this operation; never create a new transfer. Once submitted, cancellation is unavailable.",
            "body": {
              "mode": "raw",
              "raw": "{{requestBody}}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [],
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const template = {\"signedTransaction\":\"{{signedTransaction}}\"};",
                  "function resolve(value, key) {",
                  "  if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).map(([k,v]) => [k, resolve(v,k)]));",
                  "  const resolved = typeof value === 'string' ? pm.variables.replaceIn(value) : value;",
                  "  if (['qty','prizeId','shownShippingFeeUsd'].includes(key)) {",
                  "    if (resolved === '' || !Number.isFinite(Number(resolved))) throw new Error('Set a numeric ' + key + ' before sending.');",
                  "    return Number(resolved);",
                  "  }",
                  "  return resolved;",
                  "}",
                  "pm.variables.set('requestBody', JSON.stringify(resolve(template)));"
                ]
              }
            }
          ]
        },
        {
          "name": "Cancel an unsigned sellback",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{walletSession}}",
                  "type": "string"
                }
              ]
            },
            "url": {
              "raw": "{{redeemBaseUrl}}/sellbacks/{{sellbackId}}/cancel",
              "host": [
                "{{redeemBaseUrl}}"
              ],
              "path": [
                "sellbacks",
                "{{sellbackId}}",
                "cancel"
              ]
            },
            "description": "Cancel an unsigned sellback",
            "body": {
              "mode": "raw",
              "raw": "{{requestBody}}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [],
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const template = {};",
                  "function resolve(value, key) {",
                  "  if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).map(([k,v]) => [k, resolve(v,k)]));",
                  "  const resolved = typeof value === 'string' ? pm.variables.replaceIn(value) : value;",
                  "  if (['qty','prizeId','shownShippingFeeUsd'].includes(key)) {",
                  "    if (resolved === '' || !Number.isFinite(Number(resolved))) throw new Error('Set a numeric ' + key + ' before sending.');",
                  "    return Number(resolved);",
                  "  }",
                  "  return resolved;",
                  "}",
                  "pm.variables.set('requestBody', JSON.stringify(resolve(template)));"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "Redemption",
      "description": "Let a winner ship the physical prize home. **Optional.** A partner that builds\nnothing can link winners to Verity, who redeem there with the same wallet.\n\nThe prize NFT is the ticket. Verity transfers it to the wallet that paid for the\nwinning ticket at prize distribution, right after the draw, so it is already there\nwhen `prize.won` fires. The holder approves the returned transaction to hand over\nthe prize token and pay the redemption fee. Use the returned order status to show\nprogress; payment confirmation does not mean the shipment has been dispatched.\n\nThese endpoints are called **from the winner's browser**, not from your server,\nand they do not use your API key. Base URL: `/api/v1/redeem`.\n\n1. `POST /auth/nonce`, then ask the wallet to sign the returned message.\n2. `POST /auth/verify` with the signature. Use the `accessToken` as a bearer token.\n3. `GET /prizes` to list what the wallet can ship.\n4. `POST /quote` with the shipping address to show the fee. Send `prizeId` for one\n   prize, or `prizeIds` containing up to 50 distinct prizes. Mixed product types ship in one order with multiple packages.\n5. `POST /prepare` with the same selection. Ask the wallet to sign the\n   returned transaction: each transaction hands over up to eight prize NFTs and pays its share of the total fee.\n   Do not change it: Verity has already signed as fee payer, and any edit is refused.\n6. `POST /{redemptionId}/submit` with the signed transaction, inside about one\n   minute of step 5.\n7. Save the redemption ID. `GET /redemptions/{redemptionId}` resumes that order\n   after a reload or uncertain submit. Never prepare a new order while it is pending.\n   For a batch with `payment_step_count > 1`, call `POST /{redemptionId}/next-transaction`,\n   sign its `next.transaction`, and submit to the same order. Repeat until all steps are confirmed.\n   If a submitted transaction is still pending, `next` is null. Do not show repeated\n   wallet prompts while waiting for it to confirm; poll the order first. `payment_confirmed_steps`,\n   `payment_paid_usd` and `payment_signatures` describe confirmed progress. Rejected or expired\n   steps can be resumed without repeating finalized payments. Never assume a pending batch is unpaid.\n   Booking starts only after every step finalizes. Keep a visible Resume redemption action after reload.\n   Payment confirmation can return `paid_awaiting_label`; shipment booking continues asynchronously.\n   Poll one request at a time with backoff (5–30 seconds), pause in hidden tabs,\n   and honor `Retry-After` on 429. After a bounded window offer Check status.\n   Reads (`GET`) have their own allowance of 30 a minute per wallet, separate from the\n   60-an-hour budget for writes; a 429 carries `Retry-After`. A label means ready to ship,\n   not shipped. A cancellation does not itself prove a refund or no charge.\n\nGive Verity your site's origin so the browser calls pass CORS. The shipping\naddress goes from the browser to Verity. It never passes through you.\n\nEvery reply uses the envelope `{ success, data }` on success and `{ success, error }` on failure. The fields named below sit inside `data`.\n",
      "item": [
        {
          "name": "Get the message a wallet signs to sign in",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "auth": {
              "type": "noauth"
            },
            "url": {
              "raw": "{{redeemBaseUrl}}/auth/nonce",
              "host": [
                "{{redeemBaseUrl}}"
              ],
              "path": [
                "auth",
                "nonce"
              ]
            },
            "description": "Get the message a wallet signs to sign in",
            "body": {
              "mode": "raw",
              "raw": "{{requestBody}}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [],
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const template = {\"wallet\":\"{{wallet}}\",\"platform\":\"{{platformSlug}}\",\"domain\":\"{{domain}}\",\"uri\":\"{{uri}}\"};",
                  "function resolve(value, key) {",
                  "  if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).map(([k,v]) => [k, resolve(v,k)]));",
                  "  const resolved = typeof value === 'string' ? pm.variables.replaceIn(value) : value;",
                  "  if (['qty','prizeId','shownShippingFeeUsd'].includes(key)) {",
                  "    if (resolved === '' || !Number.isFinite(Number(resolved))) throw new Error('Set a numeric ' + key + ' before sending.');",
                  "    return Number(resolved);",
                  "  }",
                  "  return resolved;",
                  "}",
                  "pm.variables.set('requestBody', JSON.stringify(resolve(template)));"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const body = pm.response.json();",
                  "if (body.success) pm.collectionVariables.set('walletMessage', body.data.message);"
                ]
              }
            }
          ]
        },
        {
          "name": "Trade the signature for a session token",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "auth": {
              "type": "noauth"
            },
            "url": {
              "raw": "{{redeemBaseUrl}}/auth/verify",
              "host": [
                "{{redeemBaseUrl}}"
              ],
              "path": [
                "auth",
                "verify"
              ]
            },
            "description": "Trade the signature for a session token",
            "body": {
              "mode": "raw",
              "raw": "{{requestBody}}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [],
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const template = {\"wallet\":\"{{wallet}}\",\"message\":\"{{walletMessage}}\",\"signature\":\"{{walletSignature}}\"};",
                  "function resolve(value, key) {",
                  "  if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).map(([k,v]) => [k, resolve(v,k)]));",
                  "  const resolved = typeof value === 'string' ? pm.variables.replaceIn(value) : value;",
                  "  if (['qty','prizeId','shownShippingFeeUsd'].includes(key)) {",
                  "    if (resolved === '' || !Number.isFinite(Number(resolved))) throw new Error('Set a numeric ' + key + ' before sending.');",
                  "    return Number(resolved);",
                  "  }",
                  "  return resolved;",
                  "}",
                  "pm.variables.set('requestBody', JSON.stringify(resolve(template)));"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const body = pm.response.json();",
                  "if (body.success) pm.collectionVariables.set('walletSession', body.data.accessToken);"
                ]
              }
            }
          ]
        },
        {
          "name": "Prizes the wallet holds, has won, or has redeemed",
          "request": {
            "method": "GET",
            "header": [],
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{walletSession}}",
                  "type": "string"
                }
              ]
            },
            "url": {
              "raw": "{{redeemBaseUrl}}/prizes",
              "host": [
                "{{redeemBaseUrl}}"
              ],
              "path": [
                "prizes"
              ]
            },
            "description": "Prizes the wallet holds, has won, or has redeemed"
          },
          "response": []
        },
        {
          "name": "[Compatibility] Compatibility endpoint; not needed for pre-minted items",
          "request": {
            "method": "POST",
            "header": [],
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{walletSession}}",
                  "type": "string"
                }
              ]
            },
            "url": {
              "raw": "{{redeemBaseUrl}}/prizes/{{prizeId}}/mint-now",
              "host": [
                "{{redeemBaseUrl}}"
              ],
              "path": [
                "prizes",
                "{{prizeId}}",
                "mint-now"
              ]
            },
            "description": "Compatibility endpoint; not needed for pre-minted items\n\nNew integrations should not call this endpoint or expose a Mint/Keep Item button. For already minted prizes it returns the existing mint_status and mint_address. Older prize records may still return the eligibility or processing errors below. Use redeemable from GET /prizes to offer shipping.\n\nCompatibility only. Do not add a Mint/Keep Item UI action."
          },
          "response": []
        },
        {
          "name": "Fee lines for shipping a prize to an address",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{walletSession}}",
                  "type": "string"
                }
              ]
            },
            "url": {
              "raw": "{{redeemBaseUrl}}/quote",
              "host": [
                "{{redeemBaseUrl}}"
              ],
              "path": [
                "quote"
              ]
            },
            "description": "Fee lines for shipping a prize to an address",
            "body": {
              "mode": "raw",
              "raw": "{{requestBody}}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [],
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const template = {\"prizeId\":\"{{prizeId}}\",\"address\":{\"recipientName\":\"Example Buyer\",\"recipientEmail\":\"buyer@example.com\",\"recipientPhone\":\"{{recipientPhone}}\",\"addressLine1\":\"{{addressLine1}}\",\"addressLine2\":\"{{addressLine2}}\",\"city\":\"{{city}}\",\"stateProvince\":\"{{stateProvince}}\",\"postalCode\":\"{{postalCode}}\",\"countryCode\":\"{{countryCode}}\"}};",
                  "function resolve(value, key) {",
                  "  if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).map(([k,v]) => [k, resolve(v,k)]));",
                  "  const resolved = typeof value === 'string' ? pm.variables.replaceIn(value) : value;",
                  "  if (['qty','prizeId','shownShippingFeeUsd'].includes(key)) {",
                  "    if (resolved === '' || !Number.isFinite(Number(resolved))) throw new Error('Set a numeric ' + key + ' before sending.');",
                  "    return Number(resolved);",
                  "  }",
                  "  return resolved;",
                  "}",
                  "pm.variables.set('requestBody', JSON.stringify(resolve(template)));"
                ]
              }
            }
          ]
        },
        {
          "name": "Prepare the redemption transaction for wallet approval",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{walletSession}}",
                  "type": "string"
                }
              ]
            },
            "url": {
              "raw": "{{redeemBaseUrl}}/prepare",
              "host": [
                "{{redeemBaseUrl}}"
              ],
              "path": [
                "prepare"
              ]
            },
            "description": "Prepare the redemption transaction for wallet approval",
            "body": {
              "mode": "raw",
              "raw": "{{requestBody}}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [],
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const template = {\"prizeId\":\"{{prizeId}}\",\"address\":{\"recipientName\":\"Example Buyer\",\"recipientEmail\":\"buyer@example.com\",\"recipientPhone\":\"{{recipientPhone}}\",\"addressLine1\":\"{{addressLine1}}\",\"addressLine2\":\"{{addressLine2}}\",\"city\":\"{{city}}\",\"stateProvince\":\"{{stateProvince}}\",\"postalCode\":\"{{postalCode}}\",\"countryCode\":\"{{countryCode}}\"},\"productCode\":\"{{productCode}}\",\"shownShippingFeeUsd\":\"{{shownShippingFeeUsd}}\"};",
                  "function resolve(value, key) {",
                  "  if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).map(([k,v]) => [k, resolve(v,k)]));",
                  "  const resolved = typeof value === 'string' ? pm.variables.replaceIn(value) : value;",
                  "  if (['qty','prizeId','shownShippingFeeUsd'].includes(key)) {",
                  "    if (resolved === '' || !Number.isFinite(Number(resolved))) throw new Error('Set a numeric ' + key + ' before sending.');",
                  "    return Number(resolved);",
                  "  }",
                  "  return resolved;",
                  "}",
                  "pm.variables.set('requestBody', JSON.stringify(resolve(template)));"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const body = pm.response.json();",
                  "if (body.success) pm.collectionVariables.set('redemptionId', body.data.redemption.id);"
                ]
              }
            }
          ]
        },
        {
          "name": "Resume an existing batch without repeating confirmed payments",
          "request": {
            "method": "POST",
            "header": [],
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{walletSession}}",
                  "type": "string"
                }
              ]
            },
            "url": {
              "raw": "{{redeemBaseUrl}}/{{redemptionId}}/next-transaction",
              "host": [
                "{{redeemBaseUrl}}"
              ],
              "path": [
                "{{redemptionId}}",
                "next-transaction"
              ]
            },
            "description": "Resume an existing batch without repeating confirmed payments"
          },
          "response": []
        },
        {
          "name": "Send the signed transaction",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{walletSession}}",
                  "type": "string"
                }
              ]
            },
            "url": {
              "raw": "{{redeemBaseUrl}}/{{redemptionId}}/submit",
              "host": [
                "{{redeemBaseUrl}}"
              ],
              "path": [
                "{{redemptionId}}",
                "submit"
              ]
            },
            "description": "Send the signed transaction",
            "body": {
              "mode": "raw",
              "raw": "{{requestBody}}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [],
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const template = {\"signedTransaction\":\"{{signedTransaction}}\"};",
                  "function resolve(value, key) {",
                  "  if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).map(([k,v]) => [k, resolve(v,k)]));",
                  "  const resolved = typeof value === 'string' ? pm.variables.replaceIn(value) : value;",
                  "  if (['qty','prizeId','shownShippingFeeUsd'].includes(key)) {",
                  "    if (resolved === '' || !Number.isFinite(Number(resolved))) throw new Error('Set a numeric ' + key + ' before sending.');",
                  "    return Number(resolved);",
                  "  }",
                  "  return resolved;",
                  "}",
                  "pm.variables.set('requestBody', JSON.stringify(resolve(template)));"
                ]
              }
            }
          ]
        },
        {
          "name": "The wallet's redemptions",
          "request": {
            "method": "GET",
            "header": [],
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{walletSession}}",
                  "type": "string"
                }
              ]
            },
            "url": {
              "raw": "{{redeemBaseUrl}}/redemptions",
              "host": [
                "{{redeemBaseUrl}}"
              ],
              "path": [
                "redemptions"
              ]
            },
            "description": "The wallet's redemptions"
          },
          "response": []
        },
        {
          "name": "Status and tracking for one redemption",
          "request": {
            "method": "GET",
            "header": [],
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{walletSession}}",
                  "type": "string"
                }
              ]
            },
            "url": {
              "raw": "{{redeemBaseUrl}}/redemptions/{{redemptionId}}",
              "host": [
                "{{redeemBaseUrl}}"
              ],
              "path": [
                "redemptions",
                "{{redemptionId}}"
              ]
            },
            "description": "Status and tracking for one redemption"
          },
          "response": []
        },
        {
          "name": "Check packaging groups before requesting a quote",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{walletSession}}",
                  "type": "string"
                }
              ]
            },
            "url": {
              "raw": "{{redeemBaseUrl}}/shipping-groups",
              "host": [
                "{{redeemBaseUrl}}"
              ],
              "path": [
                "shipping-groups"
              ]
            },
            "description": "Check packaging groups before requesting a quote\n\nOptional preview for a selection of owned prizes. Send exactly one of prizeId and prizeIds. The quote remains authoritative for destination eligibility and cost.",
            "body": {
              "mode": "raw",
              "raw": "{{requestBody}}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [],
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const template = {\"prizeId\":\"{{prizeId}}\"};",
                  "function resolve(value, key) {",
                  "  if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).map(([k,v]) => [k, resolve(v,k)]));",
                  "  const resolved = typeof value === 'string' ? pm.variables.replaceIn(value) : value;",
                  "  if (['qty','prizeId','shownShippingFeeUsd'].includes(key)) {",
                  "    if (resolved === '' || !Number.isFinite(Number(resolved))) throw new Error('Set a numeric ' + key + ' before sending.');",
                  "    return Number(resolved);",
                  "  }",
                  "  return resolved;",
                  "}",
                  "pm.variables.set('requestBody', JSON.stringify(resolve(template)));"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "Webhooks (reference only)",
      "description": "Verity sends these seven events to your registered endpoint. Verify x-verity-signature over raw body bytes, acknowledge with 2xx and handle duplicates/out-of-order events. No requests in this folder are executed.\n\n### payment.updated\nDeliveries may repeat or arrive out of order. Fetch GET /payments/{operationId}\nfor authoritative state; an event is never permission to charge again.\nrefund_pending includes refund_operation_id. For partner-collected refunds,\npay once in your system then acknowledge that ID. Inventory release may emit\nreservation_released=true and refund_only=true while payment remains processing\nor review; this does not mean the payment failed or a refund has been sent.\n\n\n{\n  \"event\": \"payment.updated\",\n  \"data\": {}\n}\n\n### raffle.live\nThe raffle is now accepting ticket purchases.\n\n{\n  \"event\": \"raffle.live\",\n  \"data\": {\n    \"raffle_id\": 77,\n    \"platform_id\": 3\n  }\n}\n\n### raffle.closed\nStop accepting payments for this raffle immediately.\n\n{\n  \"event\": \"raffle.closed\",\n  \"data\": {\n    \"raffle_id\": 77,\n    \"platform_id\": 3\n  }\n}\n\n### raffle.drawn\nCarries the VRF fairness proof, identical for every platform on the raffle.\n\n`winner_external_user_ref` is `null` when the winner is not one of your buyers,\nand no partner payload ever contains a wallet address. `winner_display_name` is\nthe exception: it is shared with every platform so the result reads the same\neverywhere.\n\nVerify `vrf.provider` before presenting this as a fair draw — see the\n`fairness` schema.\n\n\n{\n  \"event\": \"raffle.drawn\",\n  \"data\": {\n    \"raffle_id\": 77,\n    \"prize_id\": 41,\n    \"winning_ticket\": \"R077-T00412\",\n    \"winner_display_name\": \"alice\",\n    \"winner_external_user_ref\": \"usr_8812\",\n    \"eligible_ticket_count\": 4210,\n    \"vrf\": {\n      \"provider\": \"orao\",\n      \"randomness_account\": \"7bXk...\",\n      \"tx_signature\": \"4Nc9...\",\n      \"random_value\": \"88231904772\"\n    }\n  }\n}\n\n### prize.won\n**This is the only handoff you get for a winner.** When it arrives, present the\nbuyback offer, and:\n\n- `verity_collected`: the prize token is already in the wallet that paid for the\n  winning ticket when this event fires. The winner ships the prize themselves\n  from their browser, by signing with that wallet against the Redemption\n  endpoints below. Link them to your prizes page;\n  `GET /users/{externalUserRef}/prizes` shows the state of each prize.\n- `partner_collected`: collect the shipping address yourself. No token is\n  minted, and the Redemption endpoints refuse the prize.\n\nFor wallet-signed sellback, sign in with the winning wallet and fetch\n`GET /buybacks` on the wallet API. Its `available` flag and expiry determine\nwhether to show the action. The event's legacy buyback fields describe the\nolder API-key endpoint only.\n\n\n{\n  \"event\": \"prize.won\",\n  \"data\": {\n    \"raffle_id\": 77,\n    \"prize_id\": 41,\n    \"prize_name\": \"Rolex Submariner 124060\",\n    \"external_user_ref\": \"usr_8812\",\n    \"display_name\": \"alice\",\n    \"buyback\": {\n      \"available\": false,\n      \"amount_usd\": null,\n      \"expires_at\": null\n    }\n  }\n}\n\n### raffle.cancelled\nLists your buyers owed a refund. `refund_tx_signature` is **always `null`** here:\nthis fires in the same transaction that creates the refund rows, before any money\nhas moved. The real signature arrives later on `refund.sent`.\n\n\n{\n  \"event\": \"raffle.cancelled\",\n  \"data\": {\n    \"raffle_id\": 77,\n    \"refunds\": [\n      {\n        \"external_user_ref\": \"usr_8812\",\n        \"tickets_purchased\": 5,\n        \"refund_amount_usd\": 250,\n        \"refund_tx_signature\": null\n      }\n    ]\n  }\n}\n\n### refund.sent\nA cancellation refund has confirmed for this buyer. For checkout refunds, track payment.updated and the refund payment operation instead.\n\n{\n  \"event\": \"refund.sent\",\n  \"data\": {\n    \"raffle_id\": 77,\n    \"external_user_ref\": \"usr_8812\",\n    \"refund_amount_usd\": 250,\n    \"signature\": \"8sT1mL9pK2wY7nH4gF6xJ5cR3dN8vB6aE9tU4yW7QbZ2\"\n  }\n}",
      "item": []
    }
  ]
}
