{"openapi":"3.1.0","info":{"title":"Tidepay API","version":"1.0.0","description":"This documentation covers the Tidepay API's endpoints for creating and managing recurring crypto subscriptions — offers, customers, subscriptions, and payments — plus the signed webhooks Tidepay sends as those subscriptions bill. See the [full guide](https://docs.tidepay.cc/docs/documentation) for concepts, authentication, sandbox mode, and webhooks.\n\nUnlike APIs with separate sandbox/production domains, Tidepay uses a single base URL for both environments — the API key you authenticate with (`X-API-Key` header) determines which one a request runs against:\n\n```plaintext\n/api/v1\n```"},"servers":[{"url":"https://api.tidepay.cc/v1","description":"Merchant API"}],"security":[{"apiKey":[]}],"tags":[{"name":"Currencies","description":"The chain + token catalog offers and payments are built against."},{"name":"Offers","description":"What a merchant sells, and on what terms — the single sellable resource. An offer carries its own pricing options and its own checkout URL."},{"name":"Wallets","description":"A merchant's registered payout wallet per token — once set for a tokenKey, it's used as the settlement destination for any offer involving that token, in place of that request's own merchantDestinationWallet."},{"name":"Customers","description":"The payer — the wallet (and later, other payment credentials) that funds a subscription."},{"name":"Subscriptions","description":"A customer's recurring relationship with an offer."},{"name":"Payments","description":"Every transaction that actually happened — direct one-off pulls, one-time offer checkouts, and recurring billing cycles alike. `source` distinguishes them; there is no separate invoices resource."},{"name":"Webhooks","description":"Your delivery endpoint and the signing secret used to verify deliveries. Both are scoped to the environment of the API key you authenticate with — see the Webhooks guide."},{"name":"Sandbox","description":"Test-mode-only tools for developing an integration."}],"x-webhooks":{"description":"Tidepay sends signed webhooks to your configured webhookUrl for: subscription.created, subscription.active, subscription.canceled, payment.succeeded, payment.failed, payment.canceled, customer.created, customer.updated, customer.deleted. Verify each request with HMAC-SHA256: signature = HMAC-SHA256(webhookSecret, `${timestamp}.${rawBody}`), sent as the X-Tidepay-Timestamp and X-Tidepay-Signature headers.","events":[{"event":"customer.created","payload":{"$ref":"#/components/schemas/CustomerCreatedWebhook"}},{"event":"customer.updated","payload":{"$ref":"#/components/schemas/CustomerUpdatedWebhook"}},{"event":"customer.deleted","payload":{"$ref":"#/components/schemas/CustomerDeletedWebhook"}},{"event":"subscription.created","payload":{"$ref":"#/components/schemas/SubscriptionCreatedWebhook"}},{"event":"subscription.active","payload":{"$ref":"#/components/schemas/SubscriptionActiveWebhook"}},{"event":"subscription.canceled","payload":{"$ref":"#/components/schemas/SubscriptionCanceledWebhook"}},{"event":"payment.succeeded","payload":{"$ref":"#/components/schemas/PaymentSucceededWebhook"}},{"event":"payment.failed","payload":{"$ref":"#/components/schemas/PaymentFailedWebhook"}},{"event":"payment.canceled","payload":{"$ref":"#/components/schemas/PaymentCanceledWebhook"}},{"event":"wallet.updated","payload":{"$ref":"#/components/schemas/WalletUpdatedWebhook"}}]},"paths":{"/currencies":{"get":{"tags":["Currencies"],"summary":"List currencies","description":"The chain + token catalog. Testnet chains are excluded by default — pass `includeTestnets=true` to include them.","parameters":[{"name":"includeTestnets","in":"query","required":false,"schema":{"type":"boolean","default":false}}],"responses":{"200":{"description":"The chain + token catalog","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListCurrenciesResponse"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"example":{"error":{"code":401,"type":"authentication_error","message":"Unauthorized"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"example":{"error":{"code":429,"type":"rate_limit_error","message":"Rate limit exceeded"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/offers":{"post":{"tags":["Offers"],"summary":"Create an offer","description":"Creates a sellable offer with one or more pricing options and returns its `checkoutUrl`. This is a single call — there is no separate plan or paylink to create first. `idempotencyKey` is optional; a retried request with the same key returns the original offer with `200` instead of creating a duplicate.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateOfferRequest"}}}},"responses":{"200":{"description":"idempotencyKey matched an existing offer — returns it unchanged","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Offer"}}}},"201":{"description":"Offer created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Offer"}}}},"400":{"description":"Invalid input — e.g. an unknown token, a repeated token, or splitEvm/splitSolana recipient addresses that don't match that list's family","content":{"application/json":{"example":{"error":{"code":400,"type":"validation_error","message":"acceptedTokens must not repeat"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"example":{"error":{"code":401,"type":"authentication_error","message":"Unauthorized"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"An offer with this identifier already exists","content":{"application/json":{"example":{"error":{"code":409,"type":"conflict","message":"An offer with identifier \"pro\" already exists"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"example":{"error":{"code":429,"type":"rate_limit_error","message":"Rate limit exceeded"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"Exchange rate unavailable — a fiat-denominated price could not be frozen. Retry shortly. Note this route returns a bare `{ error }` body rather than the typed error envelope.","content":{"application/json":{"example":{"error":"Exchange rate temporarily unavailable — try again shortly"},"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"get":{"tags":["Offers"],"summary":"List offers","parameters":[{"name":"active","in":"query","required":false,"description":"Filter to only active (true) or inactive (false) offers. Omit for both.","schema":{"type":"boolean"}},{"name":"identifier","in":"query","required":false,"description":"Look up by the merchant-chosen slug — replaces the removed GET /plans/identifier/{identifier}.","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"description":"Max 100, default 20.","schema":{"type":"integer"}},{"name":"start","in":"query","required":false,"description":"Offset for pagination, default 0.","schema":{"type":"integer"}}],"responses":{"200":{"description":"Offers for the authenticated merchant","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListOffersResponse"}}}},"400":{"description":"Invalid query parameters","content":{"application/json":{"example":{"error":{"code":400,"type":"validation_error","message":"limit must be between 1 and 100"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"example":{"error":{"code":401,"type":"authentication_error","message":"Unauthorized"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"example":{"error":{"code":429,"type":"rate_limit_error","message":"Rate limit exceeded"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/offers/{offerId}":{"get":{"tags":["Offers"],"summary":"Get an offer","parameters":[{"name":"offerId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The offer","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Offer"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"example":{"error":{"code":401,"type":"authentication_error","message":"Unauthorized"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Offer not found","content":{"application/json":{"example":{"error":{"code":404,"type":"not_found","message":"Not found"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"example":{"error":{"code":429,"type":"rate_limit_error","message":"Rate limit exceeded"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"patch":{"tags":["Offers"],"summary":"Update an offer","description":"Presentation, addressing, distribution limits and lifecycle only. `pricing` is NOT patchable: an existing option's terms are immutable, because a subscriber's on-chain authorization is sized against the amount in force when they subscribed. Add an option with `POST /offers/{offerId}/pricing`, retire one with `DELETE /offers/{offerId}/pricing/{priceId}`, and move an existing subscriber with `POST /subscriptions/{subscriptionId}/migrate`.","parameters":[{"name":"offerId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateOfferRequest"}}}},"responses":{"200":{"description":"The updated offer","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Offer"}}}},"400":{"description":"Invalid input, or an attempt to change immutable pricing terms","content":{"application/json":{"example":{"error":{"code":400,"type":"validation_error","message":"No updatable fields provided"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"example":{"error":{"code":401,"type":"authentication_error","message":"Unauthorized"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Offer not found","content":{"application/json":{"example":{"error":{"code":404,"type":"not_found","message":"Not found"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"An offer with this identifier already exists","content":{"application/json":{"example":{"error":{"code":409,"type":"conflict","message":"An offer with identifier \"pro\" already exists"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"example":{"error":{"code":429,"type":"rate_limit_error","message":"Rate limit exceeded"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"delete":{"tags":["Offers"],"summary":"Delete an offer","description":"Permanently deletes an offer. Fails with `409` while any subscription against it is still live — deactivate it instead, which hides it from new checkouts while existing subscriptions keep billing.","parameters":[{"name":"offerId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Offer deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OfferDeleted"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"example":{"error":{"code":401,"type":"authentication_error","message":"Unauthorized"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Offer not found","content":{"application/json":{"example":{"error":{"code":404,"type":"not_found","message":"Not found"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Offer has active subscriptions and cannot be deleted","content":{"application/json":{"example":{"error":{"code":409,"type":"conflict","message":"Offer has active subscriptions and cannot be deleted. Deactivate it instead."}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"example":{"error":{"code":429,"type":"rate_limit_error","message":"Rate limit exceeded"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/offers/{offerId}/pricing":{"post":{"tags":["Offers"],"summary":"Add a pricing option","description":"Adds a pricing option to an existing offer — the offer and its `checkoutUrl` stay the same, only the option list grows. This replaces the removed `POST /paylinks/{paylinkId}/variants`, which cloned an entire plan and minted a second, unrelated link.","parameters":[{"name":"offerId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddPricingOptionRequest"}}}},"responses":{"201":{"description":"The offer, with the new option included","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Offer"}}}},"400":{"description":"Invalid input","content":{"application/json":{"example":{"error":{"code":400,"type":"validation_error","message":"acceptedTokens must not repeat"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"example":{"error":{"code":401,"type":"authentication_error","message":"Unauthorized"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Offer not found","content":{"application/json":{"example":{"error":{"code":404,"type":"not_found","message":"Not found"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Duplicate label, or the offer is at its pricing-option limit","content":{"application/json":{"example":{"error":{"code":409,"type":"conflict","message":"This offer already has a pricing option labeled \"Monthly\""}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"example":{"error":{"code":429,"type":"rate_limit_error","message":"Rate limit exceeded"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"Exchange rate unavailable — a fiat-denominated price could not be frozen. Retry shortly. Note this route returns a bare `{ error }` body rather than the typed error envelope.","content":{"application/json":{"example":{"error":"Exchange rate temporarily unavailable — try again shortly"},"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/offers/{offerId}/pricing/{priceId}":{"delete":{"tags":["Offers"],"summary":"Retire a pricing option","description":"Removes the option outright when nothing has ever subscribed to it; otherwise deactivates it, so existing subscriptions' `priceId` reference stays resolvable. Either way, subscribers already on the option keep billing their agreed terms. Fails with `409` if it is the offer's last active option.","parameters":[{"name":"offerId","in":"path","required":true,"schema":{"type":"string"}},{"name":"priceId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Option retired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PricingOptionRetired"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"example":{"error":{"code":401,"type":"authentication_error","message":"Unauthorized"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Offer or pricing option not found","content":{"application/json":{"example":{"error":{"code":404,"type":"not_found","message":"Not found"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"This is the offer's last active pricing option","content":{"application/json":{"example":{"error":{"code":409,"type":"conflict","message":"An offer must keep at least one active pricing option. Deactivate the offer itself instead."}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"example":{"error":{"code":429,"type":"rate_limit_error","message":"Rate limit exceeded"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/wallets":{"post":{"tags":["Wallets"],"summary":"Register or update a wallet","description":"Upserts one wallet per `tokenKey`. An empty `wallet` string reserves the token before a real address is known and is never used as a payout destination. See [Core concepts → Wallets](https://docs.tidepay.cc/docs/documentation/concepts#wallets).","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpsertWalletRequest"}}}},"responses":{"201":{"description":"Wallet saved","content":{"application/json":{"schema":{"type":"object","properties":{"wallet":{"$ref":"#/components/schemas/MerchantWallet"}},"required":["wallet"],"additionalProperties":false}}}},"400":{"description":"Invalid input — e.g. an unknown tokenKey, or a wallet address that doesn't match that token's chain family","content":{"application/json":{"example":{"error":{"code":400,"type":"validation_error","message":"Invalid wallet address for this token's network"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"example":{"error":{"code":401,"type":"authentication_error","message":"Unauthorized"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"example":{"error":{"code":429,"type":"rate_limit_error","message":"Rate limit exceeded"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Unexpected server error","content":{"application/json":{"example":{"error":{"code":500,"type":"internal_error","message":"Failed to save settlement wallet"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"get":{"tags":["Wallets"],"summary":"List wallets","description":"One row per registered tokenKey.","responses":{"200":{"description":"Wallets for the authenticated merchant","content":{"application/json":{"example":{"wallets":[{"id":"wal_01j8x2q3r4s5t6u7v8w9x0y1z2","merchantId":"mrc_01j7f0a1b2c3d4e5f6g7h8i9j0","tokenKey":"usdc-polygon","chainId":137,"receiverWallet":"0x71c7656ec7ab88b098defb751b7401b5f6d8976","createdAt":"2026-08-01T12:00:00.000Z"},{"id":"wal_01j8x2q3r4s5t6u7v8w9x0y1z3","merchantId":"mrc_01j7f0a1b2c3d4e5f6g7h8i9j0","tokenKey":"usdt-base","chainId":8453,"receiverWallet":"0x8f3cf7ad23cd3cadbd9735aff958023239c6a063","createdAt":"2026-08-01T12:05:00.000Z"},{"id":"wal_01j8x2q3r4s5t6u7v8w9x0y1z4","merchantId":"mrc_01j7f0a1b2c3d4e5f6g7h8i9j0","tokenKey":"usdc-solana","chainId":101,"receiverWallet":"9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM","createdAt":"2026-08-01T12:10:00.000Z"}]},"schema":{"$ref":"#/components/schemas/ListWalletsResponse"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"example":{"error":{"code":401,"type":"authentication_error","message":"Unauthorized"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"example":{"error":{"code":429,"type":"rate_limit_error","message":"Rate limit exceeded"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/wallets/{walletId}":{"delete":{"tags":["Wallets"],"summary":"Delete a wallet","description":"Only succeeds while this row has no address registered yet. Once an address is set, it cannot be deleted — a wallet with a real destination falls back to any OTHER wallet you have registered for a different token before it ever falls back to an offer's own `merchantDestinationWallet`, so removing it would silently redirect payouts rather than reverting to something safe. To change where a token settles, register a different address on this same row instead (`POST /v1/wallets`, an upsert) — that fires a `wallet.updated` security notification and never leaves a gap where money is misdirected.","parameters":[{"name":"walletId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Wallet deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WalletDeleted"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"example":{"error":{"code":401,"type":"authentication_error","message":"Unauthorized"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"example":{"error":{"code":404,"type":"not_found","message":"Settlement wallet not found"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"This wallet has an address registered and cannot be deleted","content":{"application/json":{"example":{"error":{"code":409,"type":"conflict","message":"This wallet has an address registered and cannot be deleted — register a different address instead (POST /v1/wallets), which safely replaces it in place."}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"example":{"error":{"code":429,"type":"rate_limit_error","message":"Rate limit exceeded"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/customers":{"post":{"tags":["Customers"],"summary":"Create a customer","description":"`idempotencyKey` is optional — a retried request with the same key returns the original customer with `200` instead of creating a duplicate.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCustomerRequest"}}}},"responses":{"200":{"description":"idempotencyKey matched an existing customer — returns it unchanged","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomerFull"}}}},"201":{"description":"Customer created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomerFull"}}}},"400":{"description":"Invalid input","content":{"application/json":{"example":{"error":{"code":400,"type":"validation_error","message":"amount must be a positive decimal string"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"example":{"error":{"code":401,"type":"authentication_error","message":"Unauthorized"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Customer with this wallet already exists","content":{"application/json":{"example":{"error":{"code":409,"type":"conflict","message":"Customer with this wallet already exists"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"example":{"error":{"code":429,"type":"rate_limit_error","message":"Rate limit exceeded"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Unexpected server error","content":{"application/json":{"example":{"error":{"code":500,"type":"internal_error","message":"Failed to create customer"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"get":{"tags":["Customers"],"summary":"List customers","description":"Optionally filter by `?walletAddress=0x...` to look up a customer by wallet.","parameters":[{"name":"walletAddress","in":"query","required":false,"schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"description":"Max 100, default 20.","schema":{"type":"integer"}},{"name":"start","in":"query","required":false,"description":"Offset for pagination, default 0.","schema":{"type":"integer"}}],"responses":{"200":{"description":"Customers for the authenticated merchant","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListCustomersResponse"}}}},"400":{"description":"Invalid walletAddress","content":{"application/json":{"example":{"error":{"code":400,"type":"validation_error","message":"Invalid walletAddress"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"example":{"error":{"code":401,"type":"authentication_error","message":"Unauthorized"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"example":{"error":{"code":429,"type":"rate_limit_error","message":"Rate limit exceeded"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/customers/{customerId}":{"get":{"tags":["Customers"],"summary":"Get a customer","parameters":[{"name":"customerId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Customer","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Customer"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"example":{"error":{"code":401,"type":"authentication_error","message":"Unauthorized"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"example":{"error":{"code":404,"type":"not_found","message":"Not found"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"example":{"error":{"code":429,"type":"rate_limit_error","message":"Rate limit exceeded"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"patch":{"tags":["Customers"],"summary":"Update a customer","parameters":[{"name":"customerId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCustomerRequest"}}}},"responses":{"200":{"description":"Customer updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomerFull"}}}},"400":{"description":"Invalid input","content":{"application/json":{"example":{"error":{"code":400,"type":"validation_error","message":"amount must be a positive decimal string"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"example":{"error":{"code":401,"type":"authentication_error","message":"Unauthorized"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"example":{"error":{"code":404,"type":"not_found","message":"Not found"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Customer with this wallet already exists","content":{"application/json":{"example":{"error":{"code":409,"type":"conflict","message":"Customer with this wallet already exists"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"example":{"error":{"code":429,"type":"rate_limit_error","message":"Rate limit exceeded"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Unexpected server error","content":{"application/json":{"example":{"error":{"code":500,"type":"internal_error","message":"Failed to update customer"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"delete":{"tags":["Customers"],"summary":"Delete a customer","parameters":[{"name":"customerId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomerDeleted"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"example":{"error":{"code":401,"type":"authentication_error","message":"Unauthorized"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"example":{"error":{"code":404,"type":"not_found","message":"Not found"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Customer has subscriptions and cannot be deleted","content":{"application/json":{"example":{"error":{"code":409,"type":"conflict","message":"Customer has subscriptions and cannot be deleted"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"example":{"error":{"code":429,"type":"rate_limit_error","message":"Rate limit exceeded"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/subscriptions":{"post":{"tags":["Subscriptions"],"summary":"Create a subscription","description":"Requires an existing `customerId`. Returns a `subscribeUrl` where the subscriber connects a wallet and authorizes the recurring pulls on-chain — an ERC-20 allowance on EVM, or a subscription against the audited @solana/subscriptions program on Solana. See [Core concepts → Subscriptions](https://docs.tidepay.cc/docs/documentation/concepts#subscriptions).","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSubscriptionRequest"}}}},"responses":{"200":{"description":"idempotencyKey matched an existing subscription — returns it unchanged","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSubscriptionResponse"}}}},"201":{"description":"Subscription created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSubscriptionResponse"}}}},"400":{"description":"Invalid input","content":{"application/json":{"example":{"error":{"code":400,"type":"validation_error","message":"amount must be a positive decimal string"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"example":{"error":{"code":401,"type":"authentication_error","message":"Unauthorized"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Plan or customer not found","content":{"application/json":{"example":{"error":{"code":404,"type":"not_found","message":"Plan not found"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"This customer already has a subscription to this plan (and no idempotencyKey was provided, or it didn't match)","content":{"application/json":{"example":{"error":{"code":409,"type":"conflict","message":"This customer already has a subscription to this plan"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"example":{"error":{"code":429,"type":"rate_limit_error","message":"Rate limit exceeded"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Unexpected server error","content":{"application/json":{"example":{"error":{"code":500,"type":"internal_error","message":"Failed to create subscription"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"get":{"tags":["Subscriptions"],"summary":"List subscriptions","description":"Filter by `status`, `customerId`, and/or `offerId`.","parameters":[{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["pending","active","past_due","canceled"]}},{"name":"customerId","in":"query","required":false,"schema":{"type":"string"}},{"name":"offerId","in":"query","required":false,"schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"description":"Max 100, default 20.","schema":{"type":"integer"}},{"name":"start","in":"query","required":false,"description":"Offset for pagination, default 0.","schema":{"type":"integer"}}],"responses":{"200":{"description":"Subscriptions for the authenticated merchant","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListSubscriptionsResponse"}}}},"400":{"description":"Invalid query parameters","content":{"application/json":{"example":{"error":{"code":400,"type":"validation_error","message":"limit must be between 1 and 100"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"example":{"error":{"code":401,"type":"authentication_error","message":"Unauthorized"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"example":{"error":{"code":429,"type":"rate_limit_error","message":"Rate limit exceeded"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/subscriptions/{subscriptionId}":{"get":{"tags":["Subscriptions"],"summary":"Get subscription status","description":"Includes `cyclesCompleted`, `consecutiveFailures`, and `customVariables`.","parameters":[{"name":"subscriptionId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Subscription + last invoice","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubscriptionDetail"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"example":{"error":{"code":401,"type":"authentication_error","message":"Unauthorized"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"example":{"error":{"code":404,"type":"not_found","message":"Not found"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"example":{"error":{"code":429,"type":"rate_limit_error","message":"Rate limit exceeded"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"patch":{"tags":["Subscriptions"],"summary":"Update a subscription","description":"Only `customVariables` is mutable.","parameters":[{"name":"subscriptionId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSubscriptionRequest"}}}},"responses":{"200":{"description":"Updated subscription","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubscriptionListItem"}}}},"400":{"description":"Invalid input","content":{"application/json":{"example":{"error":{"code":400,"type":"validation_error","message":"Invalid input"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"example":{"error":{"code":401,"type":"authentication_error","message":"Unauthorized"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"example":{"error":{"code":404,"type":"not_found","message":"Not found"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"example":{"error":{"code":429,"type":"rate_limit_error","message":"Rate limit exceeded"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/subscriptions/{subscriptionId}/cancel":{"post":{"tags":["Subscriptions"],"summary":"Cancel a subscription","description":"Stops future billing. Cancelling is not deletion — the subscription, its pricing snapshot, and every payment it produced survive, because that history is what reconciliation reads. Idempotent: cancelling an already-canceled subscription succeeds. The subscriber's on-chain authorization is theirs to revoke, so resuming later may require them to re-authorize.","parameters":[{"name":"subscriptionId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Canceled","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubscriptionCanceled"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"example":{"error":{"code":401,"type":"authentication_error","message":"Unauthorized"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"example":{"error":{"code":404,"type":"not_found","message":"Not found"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"example":{"error":{"code":429,"type":"rate_limit_error","message":"Rate limit exceeded"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/subscriptions/{subscriptionId}/resume":{"post":{"tags":["Subscriptions"],"summary":"Resume a subscription","description":"Two outcomes, reported via `status` and `reauthorizationRequired`. A **past_due** subscription reactivates immediately — its authorization was never revoked, so no customer action is needed. A **canceled** one returns an `authorizationUrl` and moves to `pending_authorization`: cancelling revokes the EVM allowance / closes the Solana delegation account, and only the wallet holder can sign a new one. Billing does not resume until they do.","parameters":[{"name":"subscriptionId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Resumed, or awaiting the customer's re-authorization — check `reauthorizationRequired`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubscriptionResumed"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"example":{"error":{"code":401,"type":"authentication_error","message":"Unauthorized"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"example":{"error":{"code":404,"type":"not_found","message":"Not found"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Already awaiting authorization, or the pricing option has been retired","content":{"application/json":{"example":{"error":{"code":409,"type":"conflict","message":"This subscription's pricing option has been retired and it cannot be resumed. Create a new subscription instead."}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"example":{"error":{"code":429,"type":"rate_limit_error","message":"Rate limit exceeded"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/subscriptions/{subscriptionId}/migrate":{"post":{"tags":["Subscriptions"],"summary":"Migrate to a different pricing option","description":"Moves a subscriber to another pricing option **on the same offer**. Deliberately two-step: the new terms cannot take effect until the customer re-authorizes on-chain, since an EVM allowance is sized against the amount granted and Solana stores it immutably. This records the intent and returns an `authorizationUrl`; the subscription keeps billing its CURRENT terms until the customer completes it. An abandoned migration changes nothing — there is no path where a subscriber is charged more than they agreed to.","parameters":[{"name":"subscriptionId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MigrateSubscriptionRequest"}}}},"responses":{"200":{"description":"Migration pending the customer's authorization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubscriptionMigrating"}}}},"400":{"description":"Target option belongs to another offer, or is one-time","content":{"application/json":{"example":{"error":{"code":400,"type":"validation_error","message":"The target pricing option belongs to a different offer. Migration only moves a subscriber between options on the same offer."}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"example":{"error":{"code":401,"type":"authentication_error","message":"Unauthorized"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Subscription or pricing option not found","content":{"application/json":{"example":{"error":{"code":404,"type":"not_found","message":"Pricing option not found"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Subscription is canceled, already on that option, or the option is retired","content":{"application/json":{"example":{"error":{"code":409,"type":"conflict","message":"This subscription is already on that pricing option"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"example":{"error":{"code":429,"type":"rate_limit_error","message":"Rate limit exceeded"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/subscriptions/{subscriptionId}/retry":{"post":{"tags":["Subscriptions"],"summary":"Retry subscription invoice cycle","description":"Manually triggers an immediate retry without waiting for the next cron run.","parameters":[{"name":"subscriptionId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Invoice cycle retry result (`settled`, `failed`, or `skipped`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubscriptionRetryResponse"}}}},"400":{"description":"Cannot retry a canceled subscription","content":{"application/json":{"example":{"error":{"code":400,"type":"validation_error","message":"Cannot retry a canceled subscription"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"example":{"error":{"code":401,"type":"authentication_error","message":"Unauthorized"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Subscription not found","content":{"application/json":{"example":{"error":{"code":404,"type":"not_found","message":"Subscription not found"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"example":{"error":{"code":429,"type":"rate_limit_error","message":"Rate limit exceeded"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Unexpected server error","content":{"application/json":{"example":{"error":{"code":500,"type":"internal_error","message":"Failed to retry invoice cycle"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/payments":{"post":{"tags":["Payments"],"summary":"Create a one-off payment","description":"Pulls `amount` of `tokenKey` from `fromWallet` and forwards it to `toWallet`. `idempotencyKey` is required. See [Core concepts → Payments](https://docs.tidepay.cc/docs/documentation/concepts#payments).","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatePaymentRequest"}}}},"responses":{"200":{"description":"A payment with this idempotencyKey already exists — returns the original result","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Payment"}}}},"201":{"description":"Payment created and executed (see `status` for the outcome). For `paymentMethod: \"wallet_transfer\"`, the response also includes `depositAddress`/`depositExpiresAt` (see `PaymentWalletTransfer`) — this is the only response that ever returns those two fields.","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/Payment"},{"$ref":"#/components/schemas/PaymentWalletTransfer"}]}}}},"400":{"description":"Invalid input","content":{"application/json":{"example":{"error":{"code":400,"type":"validation_error","message":"amount must be a positive decimal string"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"example":{"error":{"code":401,"type":"authentication_error","message":"Unauthorized"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"idempotencyKey already used by a different merchant","content":{"application/json":{"example":{"error":{"code":409,"type":"conflict","message":"idempotencyKey already used by a different merchant"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"example":{"error":{"code":429,"type":"rate_limit_error","message":"Rate limit exceeded"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Unexpected server error","content":{"application/json":{"example":{"error":{"code":500,"type":"internal_error","message":"Failed to create payment"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"get":{"tags":["Payments"],"summary":"List payments","parameters":[{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["pending","pulled","settled","failed","canceled"]}},{"name":"source","in":"query","required":false,"description":"Filter to direct charges and one-time offer checkouts (\"one_time\") or recurring billing cycles (\"subscription\"). Omit for both.","schema":{"type":"string","enum":["one_time","subscription"]}},{"name":"customerId","in":"query","required":false,"schema":{"type":"string"}},{"name":"subscriptionId","in":"query","required":false,"schema":{"type":"string"}},{"name":"offerId","in":"query","required":false,"schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"description":"Max 100, default 20.","schema":{"type":"integer"}},{"name":"start","in":"query","required":false,"description":"Offset for pagination, default 0.","schema":{"type":"integer"}}],"responses":{"200":{"description":"Payments for the authenticated merchant","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListPaymentsResponse"}}}},"400":{"description":"Invalid query parameters","content":{"application/json":{"example":{"error":{"code":400,"type":"validation_error","message":"limit must be between 1 and 100"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"example":{"error":{"code":401,"type":"authentication_error","message":"Unauthorized"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"example":{"error":{"code":429,"type":"rate_limit_error","message":"Rate limit exceeded"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/payments/{paymentId}":{"get":{"tags":["Payments"],"summary":"Get a payment","description":"For a `wallet_transfer` payment, `depositAddress`/`depositExpiresAt` are only present on the immediate `POST /payments` response, not here.","parameters":[{"name":"paymentId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Payment","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Payment"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"example":{"error":{"code":401,"type":"authentication_error","message":"Unauthorized"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"example":{"error":{"code":404,"type":"not_found","message":"Not found"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"example":{"error":{"code":429,"type":"rate_limit_error","message":"Rate limit exceeded"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/payments/{paymentId}/cancel":{"post":{"tags":["Payments"],"summary":"Cancel a payment","description":"Only reachable while the payment is still \"pending\".","parameters":[{"name":"paymentId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Canceled payment","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Payment"}}}},"400":{"description":"Cannot cancel a payment with status \"pulled\" — only \"pending\" payments can be canceled","content":{"application/json":{"example":{"error":{"code":400,"type":"validation_error","message":"Cannot cancel this payment"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"example":{"error":{"code":401,"type":"authentication_error","message":"Unauthorized"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"example":{"error":{"code":404,"type":"not_found","message":"Not found"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"example":{"error":{"code":429,"type":"rate_limit_error","message":"Rate limit exceeded"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/webhooks/endpoint":{"get":{"tags":["Webhooks"],"summary":"Get webhook endpoint","description":"The delivery endpoint for the environment of the key you authenticate with. A test key reports the sandbox endpoint, a live key the live one.","responses":{"200":{"description":"The endpoint for this environment","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEndpoint"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"example":{"error":{"code":401,"type":"authentication_error","message":"Unauthorized"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"example":{"error":{"code":429,"type":"rate_limit_error","message":"Rate limit exceeded"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"patch":{"tags":["Webhooks"],"summary":"Set webhook endpoint","description":"Sets the delivery endpoint for this key's environment — a test key writes the sandbox endpoint, a live key the live one, and neither can touch the other. Send an empty string to unset it. Useful for pointing a sandbox integration at an ephemeral tunnel from CI.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateWebhookEndpointRequest"}}}},"responses":{"200":{"description":"The updated endpoint","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEndpoint"}}}},"400":{"description":"Invalid input — a malformed URL, or one resolving to a private, loopback, or link-local address","content":{"application/json":{"example":{"error":{"code":400,"type":"validation_error","message":"webhookUrl must not point at a private address"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"example":{"error":{"code":401,"type":"authentication_error","message":"Unauthorized"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"example":{"error":{"code":429,"type":"rate_limit_error","message":"Rate limit exceeded"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/webhooks/secret":{"get":{"tags":["Webhooks"],"summary":"Get signing secret status","description":"Reports whether a signing secret exists, without returning it.","responses":{"200":{"description":"Signing secret status","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookSecretStatus"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"example":{"error":{"code":401,"type":"authentication_error","message":"Unauthorized"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"example":{"error":{"code":429,"type":"rate_limit_error","message":"Rate limit exceeded"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/webhooks/secret/rotate":{"post":{"tags":["Webhooks"],"summary":"Rotate signing secret","description":"Generates a new signing secret and immediately replaces the old one. Returned in plaintext only this once — store it now.","responses":{"200":{"description":"New signing secret","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookSecretRotated"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"example":{"error":{"code":401,"type":"authentication_error","message":"Unauthorized"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"example":{"error":{"code":429,"type":"rate_limit_error","message":"Rate limit exceeded"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/sandbox/advance":{"post":{"tags":["Sandbox"],"summary":"Advance a sandbox subscription's test clock","description":"Test-key only. Fast-forwards a sandbox subscription through up to 12 billing periods, running the renewal charge for each — the equivalent of Stripe's test clocks. Stops at the first cycle that does not settle, so a test wallet that forces a failure leaves the subscription `past_due`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"subscriptionId":{"type":"string"},"cycles":{"type":"integer","minimum":1,"maximum":12,"default":1}},"required":["subscriptionId"]}}}},"responses":{"200":{"description":"Outcome of each billing cycle run","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SandboxAdvanceResponse"}}}},"400":{"description":"Invalid input","content":{"application/json":{"example":{"error":{"code":400,"type":"validation_error","message":"cycles must be an integer from 1 to 12"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"example":{"error":{"code":401,"type":"authentication_error","message":"Unauthorized"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Requires a test API key","content":{"application/json":{"example":{"error":{"code":403,"type":"forbidden","message":"Requires a test API key"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Sandbox subscription not found","content":{"application/json":{"example":{"error":{"code":404,"type":"not_found","message":"Subscription not found"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"example":{"error":{"code":429,"type":"rate_limit_error","message":"Rate limit exceeded"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/sandbox/charge":{"post":{"tags":["Sandbox"],"summary":"Force-run a sandbox invoice cycle","description":"Test-key only. Force-runs one invoice cycle right now instead of waiting for the cron.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"subscriptionId":{"type":"string"}},"required":["subscriptionId"]}}}},"responses":{"200":{"description":"Invoice cycle outcome","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SandboxChargeResponse"}}}},"400":{"description":"Missing subscriptionId","content":{"application/json":{"example":{"error":{"code":400,"type":"validation_error","message":"subscriptionId required"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"example":{"error":{"code":401,"type":"authentication_error","message":"Unauthorized"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Requires a test API key","content":{"application/json":{"example":{"error":{"code":403,"type":"forbidden","message":"Requires a test API key"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Sandbox subscription not found","content":{"application/json":{"example":{"error":{"code":404,"type":"not_found","message":"Subscription not found"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"example":{"error":{"code":429,"type":"rate_limit_error","message":"Rate limit exceeded"}},"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}}},"components":{"schemas":{"CreateOfferRequest":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200,"description":"What you are selling, shown as the heading at checkout. This is also what a subscription snapshots as its display name, so it survives the offer being renamed later.","examples":["Pro"]},"description":{"description":"Shown at checkout beneath the name. Plain text — no markdown or HTML rendering.","examples":["Monthly access to all premium features"],"type":"string","maxLength":1000},"imageUrl":{"description":"Cover image shown at checkout. Must be a publicly reachable HTTPS URL — Tidepay does not host or proxy it.","examples":["https://example.com/checkout-banner.png"],"type":"string","format":"uri"},"identifier":{"description":"Your own stable slug for this offer, so you can address it without storing Tidepay's opaque id: GET /v1/offers?identifier=pro. Unique per merchant — reusing one returns 409. Lowercase letters, numbers, underscores and hyphens only.","examples":["pro"],"type":"string","minLength":1,"maxLength":100,"pattern":"^[a-z0-9_-]+$"},"productRef":{"description":"Free-text reference to this product in your own catalog. Purely informational — never validated for uniqueness or format, and never interpreted by Tidepay. Use `identifier` if you need to look the offer up.","examples":["SKU-1234"],"type":"string","maxLength":200},"features":{"description":"Descriptive bullet points listed at checkout (e.g. \"2 classes per week\"). Display and entitlement metadata only — these NEVER affect what is billed. Max 20 entries.","maxItems":20,"type":"array","items":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200,"description":"Label shown to the buyer at checkout.","examples":["Classes per week"]},"identifier":{"type":"string","minLength":1,"maxLength":100,"description":"Your own key for this feature, for matching it against your entitlements after purchase.","examples":["classes"]},"value":{"type":"integer","exclusiveMinimum":0,"maximum":1000000,"description":"Whole-number quantity for this feature.","examples":[2]}},"required":["name","identifier","value"]}},"collectFields":{"description":"Personal details the checkout collects from the payer before they connect a wallet. Every field is opt-in: omit a key (or the whole object) and it is not asked for. Only request what you actually need — each field is one more step between the buyer and paying.","examples":[{"email":true}],"type":"object","properties":{"fullName":{"description":"Ask for the payer's full name.","type":"boolean"},"email":{"description":"Ask for the payer's email. Set this if you need to contact them about the purchase — Tidepay does not collect it otherwise.","type":"boolean"},"dob":{"description":"Ask for date of birth (YYYY-MM-DD).","type":"boolean"},"pob":{"description":"Ask for place of birth (free text).","type":"boolean"},"address":{"description":"Ask for a structured postal address.","type":"boolean"},"phone":{"description":"Ask for a phone number, normalized to E.164.","type":"boolean"}}},"allowedPaymentMethods":{"description":"Which checkout methods this offer accepts. Omit for both. Enabling walletTransfer on an offer with recurring pricing is rejected: a recurring charge needs a standing on-chain authorization, which only walletConnect grants.","examples":[{"walletConnect":true,"walletTransfer":false}],"type":"object","properties":{"walletConnect":{"type":"boolean","description":"The payer connects a wallet and authorizes the charge on-chain. Required for anything recurring."},"walletTransfer":{"type":"boolean","description":"The payer manually sends funds to a one-off deposit address (e.g. by scanning a QR code) — no wallet connection needed. Only valid when every pricing option on this offer is one-time."}},"required":["walletConnect","walletTransfer"]},"availableQuantity":{"description":"Total successful checkouts this offer allows, ever — not a remaining count (track that via `redeemedCount` in the response). Once reached, the offer deactivates itself and its checkout URL returns 404. Omit for unlimited.","examples":[100],"type":"integer","exclusiveMinimum":0,"maximum":1000000},"expiresAt":{"description":"ISO-8601 timestamp after which checkout returns 404, exactly as a deactivated offer does. Existing subscriptions keep billing — this closes the offer to NEW buyers, it does not end what was already sold.","examples":["2026-12-31T23:59:59.000Z"],"type":"string"},"afterCompletion":{"description":"Where the buyer lands once payment settles: a hosted success page (optionally with your own message), or a redirect to a URL you own. Defaults to the hosted page.","$ref":"#/components/schemas/AfterCompletion"},"metadata":{"description":"Arbitrary key-value data echoed back on every response and webhook for this offer. Never read or interpreted by Tidepay — it exists so you can correlate an offer with your own systems.","examples":[{"orderId":"12345"}],"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},"reference":{"description":"Short external reference string, e.g. your own order id. Not required to be unique, and not usable to look the offer up — see `identifier` for that.","examples":["ORDER-2024-001"],"type":"string","maxLength":200},"pricing":{"minItems":1,"maxItems":20,"type":"array","items":{"type":"object","properties":{"label":{"description":"Names this option at checkout when the offer has several (e.g. \"Monthly\" vs \"Annual\"). Must be unique within an offer. Omit and the checkout derives a label from the billing interval.","examples":["Monthly"],"type":"string","minLength":1,"maxLength":100},"amount":{"type":"string","pattern":"^\\d+(\\.\\d+)?$","description":"How much of `currency` this option costs, as a decimal string — e.g. amount \"30\" with currency \"USD\" means $30.00. This pair is the shop-window price; it is NOT necessarily the number of tokens pulled on-chain (see `currency`'s description for that). Minimum $1.00 USD equivalent. IMMUTABLE once created: an existing subscriber's on-chain authorization is sized against the token amount derived from this, so it can never be edited. To charge differently, add another pricing option.","examples":["30"]},"currency":{"description":"What `amount` is denominated in — this decides whether `amount` itself is the on-chain charge or just the price it is converted from. A stablecoin ticker (USDC, USDT, EURC) means `amount` IS the token amount: currency \"USDC\" + amount \"30\" pulls exactly 30 USDC, every cycle. A fiat code (USD, BRL, MXN, COP, ARS, ...) means `amount` is a fiat price that gets converted to each entry in `acceptedTokens` ONCE, at creation, using the exchange rate at that moment — currency \"USD\" + amount \"30\" on a plan accepting usdc-base freezes to however many USDC $30 buys right then (see `acceptedTokens` in the response for that frozen figure), and the buyer pays that same token amount every cycle afterwards regardless of what the USD/USDC rate does later.","examples":["USDC"],"$ref":"#/components/schemas/PlanCurrency"},"acceptedTokens":{"minItems":1,"maxItems":13,"type":"array","items":{"type":"string","enum":["usdc-polygon","usdt-polygon","usdc-arbitrum","usdt-arbitrum","usdc-base","eurc-base","usdc-optimism","usdt-optimism","usdc-base-sepolia","usdc-solana","usdt-solana","eurc-solana","usdc-solana-devnet"]},"description":"Every chain+token combination this option accepts, as catalog keys from GET /v1/currencies. The buyer picks one at checkout — both the chain AND the token. Each gets its own frozen amount. Must not repeat.","examples":[["usdc-base"]]},"billing":{"oneOf":[{"type":"object","properties":{"type":{"type":"string","const":"one_time","description":"Charged once. Produces a Payment directly, with no Subscription and no billing schedule."}},"required":["type"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string","const":"recurring","description":"Charged on a schedule. Produces a Subscription, which then produces one Payment per cycle."},"interval":{"type":"string","enum":["day","week","month","year"],"description":"Billing cadence. Combined with intervalCount: { interval: \"month\", intervalCount: 3 } bills quarterly.","examples":["month"]},"intervalCount":{"description":"Bills every N intervals. Omit for every interval (i.e. 1).","examples":[1],"type":"integer","exclusiveMinimum":0,"maximum":52}},"required":["type","interval"],"additionalProperties":false}],"description":"One-time or recurring, and how often. A one-time option produces a Payment; a recurring one produces a Subscription that then produces a Payment per cycle. IMMUTABLE once created, same reason as `amount`.","type":"object"},"splitEvm":{"description":"Split each EVM payout among several recipients, via a 0xSplits contract deployed per accepted EVM chain. Addresses must be EVM (0x…). Tidepay's own take rate is injected as an extra recipient and your percentages are rescaled to fit. IMMUTABLE — the recipient list is baked into the contract address subscribers authorize against.","$ref":"#/components/schemas/CreatePlanSplitEvmRequest"},"splitSolana":{"description":"Split each Solana payout among several recipients. Solana has no split contract — the operator fans out one transfer per recipient at distribute time. Addresses must be Solana (base58). Same take-rate injection and immutability as splitEvm.","$ref":"#/components/schemas/CreatePlanSplitSolanaRequest"},"merchantDestinationWallet":{"description":"Payout address for this option. Falls back to your registered settlement wallet for the chosen token.","anyOf":[{"type":"string","pattern":"^0x[a-fA-F0-9]{40}$"},{"type":"string","pattern":"^[1-9A-HJ-NP-Za-km-z]{32,44}$"}]},"trialDays":{"examples":[7],"type":"integer","minimum":0,"maximum":365},"maxCycles":{"examples":[12],"type":"integer","exclusiveMinimum":0,"maximum":100000},"maxFailedCycles":{"examples":[3],"type":"integer","exclusiveMinimum":0,"maximum":1000}},"required":["amount","acceptedTokens","billing"],"additionalProperties":false},"description":"One or more pricing options. The customer picks one at checkout."},"idempotencyKey":{"description":"Your own key for this logical operation. Retrying with the same key returns the ORIGINAL offer with 200 instead of creating a second one — and, critically, without re-creating its pricing options or re-publishing their on-chain objects. Scoped per merchant and per live/test mode.","examples":["550e8400-e29b-41d4-a716-446655440000"],"type":"string","minLength":1,"maxLength":200}},"required":["name","pricing"],"additionalProperties":false},"AfterCompletion":{"oneOf":[{"type":"object","properties":{"type":{"type":"string","const":"hosted"},"customMessage":{"examples":["Thanks for subscribing!"],"type":"string","maxLength":500}},"required":["type"]},{"type":"object","properties":{"type":{"type":"string","const":"redirect"},"redirectUrl":{"type":"string","format":"uri","examples":["https://example.com/thank-you"]}},"required":["type","redirectUrl"]}],"type":"object"},"PlanCurrency":{"default":"USD","description":"Currency the plan's amount is denominated in. Fiat codes are converted to the settlement token at each invoice using the current exchange rate, so the token amount pulled varies between cycles. A stablecoin ticker means the amount is already a token amount and is charged as-is.","type":"string","enum":["USD","EUR","BRL","GBP","MXN","COP","ARS","CLP","PEN","UYU","CAD","AUD","JPY","CHF","AED","SGD","HKD","INR","TRY","ZAR","USDC","USDT","EURC"]},"CreatePlanSplitEvmRequest":{"type":"object","properties":{"recipients":{"minItems":2,"maxItems":19,"type":"array","items":{"$ref":"#/components/schemas/SplitRecipient"}},"distributorFeePercent":{"default":0,"examples":[0],"type":"number","minimum":0,"maximum":10}},"required":["recipients"]},"SplitRecipient":{"type":"object","properties":{"address":{"anyOf":[{"type":"string","pattern":"^0x[a-fA-F0-9]{40}$"},{"type":"string","pattern":"^[1-9A-HJ-NP-Za-km-z]{32,44}$"}]},"percentAllocation":{"type":"number","exclusiveMinimum":0,"maximum":100,"examples":[50]}},"required":["address","percentAllocation"]},"CreatePlanSplitSolanaRequest":{"type":"object","properties":{"recipients":{"minItems":2,"maxItems":9,"type":"array","items":{"$ref":"#/components/schemas/SplitRecipient"}},"distributorFeePercent":{"default":0,"examples":[0],"type":"number","minimum":0,"maximum":10}},"required":["recipients"]},"UpdateOfferRequest":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200,"description":"What you are selling, shown as the heading at checkout. This is also what a subscription snapshots as its display name, so it survives the offer being renamed later.","examples":["Pro"]},"description":{"description":"Shown at checkout beneath the name. Plain text — no markdown or HTML rendering.","examples":["Monthly access to all premium features"],"type":"string","maxLength":1000},"imageUrl":{"description":"Cover image shown at checkout. Must be a publicly reachable HTTPS URL — Tidepay does not host or proxy it.","examples":["https://example.com/checkout-banner.png"],"type":"string","format":"uri"},"identifier":{"description":"Your own stable slug for this offer, so you can address it without storing Tidepay's opaque id: GET /v1/offers?identifier=pro. Unique per merchant — reusing one returns 409. Lowercase letters, numbers, underscores and hyphens only.","examples":["pro"],"type":"string","minLength":1,"maxLength":100,"pattern":"^[a-z0-9_-]+$"},"productRef":{"description":"Free-text reference to this product in your own catalog. Purely informational — never validated for uniqueness or format, and never interpreted by Tidepay. Use `identifier` if you need to look the offer up.","examples":["SKU-1234"],"type":"string","maxLength":200},"features":{"description":"Descriptive bullet points listed at checkout (e.g. \"2 classes per week\"). Display and entitlement metadata only — these NEVER affect what is billed. Max 20 entries.","maxItems":20,"type":"array","items":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200,"description":"Label shown to the buyer at checkout.","examples":["Classes per week"]},"identifier":{"type":"string","minLength":1,"maxLength":100,"description":"Your own key for this feature, for matching it against your entitlements after purchase.","examples":["classes"]},"value":{"type":"integer","exclusiveMinimum":0,"maximum":1000000,"description":"Whole-number quantity for this feature.","examples":[2]}},"required":["name","identifier","value"]}},"collectFields":{"description":"Personal details the checkout collects from the payer before they connect a wallet. Every field is opt-in: omit a key (or the whole object) and it is not asked for. Only request what you actually need — each field is one more step between the buyer and paying.","examples":[{"email":true}],"type":"object","properties":{"fullName":{"description":"Ask for the payer's full name.","type":"boolean"},"email":{"description":"Ask for the payer's email. Set this if you need to contact them about the purchase — Tidepay does not collect it otherwise.","type":"boolean"},"dob":{"description":"Ask for date of birth (YYYY-MM-DD).","type":"boolean"},"pob":{"description":"Ask for place of birth (free text).","type":"boolean"},"address":{"description":"Ask for a structured postal address.","type":"boolean"},"phone":{"description":"Ask for a phone number, normalized to E.164.","type":"boolean"}}},"allowedPaymentMethods":{"description":"Which checkout methods this offer accepts. Omit for both. Enabling walletTransfer on an offer with recurring pricing is rejected: a recurring charge needs a standing on-chain authorization, which only walletConnect grants.","examples":[{"walletConnect":true,"walletTransfer":false}],"type":"object","properties":{"walletConnect":{"type":"boolean","description":"The payer connects a wallet and authorizes the charge on-chain. Required for anything recurring."},"walletTransfer":{"type":"boolean","description":"The payer manually sends funds to a one-off deposit address (e.g. by scanning a QR code) — no wallet connection needed. Only valid when every pricing option on this offer is one-time."}},"required":["walletConnect","walletTransfer"]},"availableQuantity":{"description":"Total successful checkouts this offer allows, ever — not a remaining count (track that via `redeemedCount` in the response). Once reached, the offer deactivates itself and its checkout URL returns 404. Omit for unlimited.","examples":[100],"type":"integer","exclusiveMinimum":0,"maximum":1000000},"expiresAt":{"description":"ISO-8601 timestamp after which checkout returns 404, exactly as a deactivated offer does. Existing subscriptions keep billing — this closes the offer to NEW buyers, it does not end what was already sold.","examples":["2026-12-31T23:59:59.000Z"],"type":"string"},"afterCompletion":{"description":"Where the buyer lands once payment settles: a hosted success page (optionally with your own message), or a redirect to a URL you own. Defaults to the hosted page.","$ref":"#/components/schemas/AfterCompletion"},"metadata":{"description":"Arbitrary key-value data echoed back on every response and webhook for this offer. Never read or interpreted by Tidepay — it exists so you can correlate an offer with your own systems.","examples":[{"orderId":"12345"}],"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},"reference":{"description":"Short external reference string, e.g. your own order id. Not required to be unique, and not usable to look the offer up — see `identifier` for that.","examples":["ORDER-2024-001"],"type":"string","maxLength":200},"active":{"description":"Deactivating hides the offer from new checkouts; existing subscriptions keep billing.","examples":[true],"type":"boolean"}},"additionalProperties":false},"AddPricingOptionRequest":{"type":"object","properties":{"label":{"description":"Names this option at checkout when the offer has several (e.g. \"Monthly\" vs \"Annual\"). Must be unique within an offer. Omit and the checkout derives a label from the billing interval.","examples":["Monthly"],"type":"string","minLength":1,"maxLength":100},"amount":{"type":"string","pattern":"^\\d+(\\.\\d+)?$","description":"How much of `currency` this option costs, as a decimal string — e.g. amount \"30\" with currency \"USD\" means $30.00. This pair is the shop-window price; it is NOT necessarily the number of tokens pulled on-chain (see `currency`'s description for that). Minimum $1.00 USD equivalent. IMMUTABLE once created: an existing subscriber's on-chain authorization is sized against the token amount derived from this, so it can never be edited. To charge differently, add another pricing option.","examples":["30"]},"currency":{"description":"What `amount` is denominated in — this decides whether `amount` itself is the on-chain charge or just the price it is converted from. A stablecoin ticker (USDC, USDT, EURC) means `amount` IS the token amount: currency \"USDC\" + amount \"30\" pulls exactly 30 USDC, every cycle. A fiat code (USD, BRL, MXN, COP, ARS, ...) means `amount` is a fiat price that gets converted to each entry in `acceptedTokens` ONCE, at creation, using the exchange rate at that moment — currency \"USD\" + amount \"30\" on a plan accepting usdc-base freezes to however many USDC $30 buys right then (see `acceptedTokens` in the response for that frozen figure), and the buyer pays that same token amount every cycle afterwards regardless of what the USD/USDC rate does later.","examples":["USDC"],"$ref":"#/components/schemas/PlanCurrency"},"acceptedTokens":{"minItems":1,"maxItems":13,"type":"array","items":{"type":"string","enum":["usdc-polygon","usdt-polygon","usdc-arbitrum","usdt-arbitrum","usdc-base","eurc-base","usdc-optimism","usdt-optimism","usdc-base-sepolia","usdc-solana","usdt-solana","eurc-solana","usdc-solana-devnet"]},"description":"Every chain+token combination this option accepts, as catalog keys from GET /v1/currencies. The buyer picks one at checkout — both the chain AND the token. Each gets its own frozen amount. Must not repeat.","examples":[["usdc-base"]]},"billing":{"oneOf":[{"type":"object","properties":{"type":{"type":"string","const":"one_time","description":"Charged once. Produces a Payment directly, with no Subscription and no billing schedule."}},"required":["type"],"additionalProperties":false},{"type":"object","properties":{"type":{"type":"string","const":"recurring","description":"Charged on a schedule. Produces a Subscription, which then produces one Payment per cycle."},"interval":{"type":"string","enum":["day","week","month","year"],"description":"Billing cadence. Combined with intervalCount: { interval: \"month\", intervalCount: 3 } bills quarterly.","examples":["month"]},"intervalCount":{"description":"Bills every N intervals. Omit for every interval (i.e. 1).","examples":[1],"type":"integer","exclusiveMinimum":0,"maximum":52}},"required":["type","interval"],"additionalProperties":false}],"description":"One-time or recurring, and how often. A one-time option produces a Payment; a recurring one produces a Subscription that then produces a Payment per cycle. IMMUTABLE once created, same reason as `amount`.","type":"object"},"splitEvm":{"description":"Split each EVM payout among several recipients, via a 0xSplits contract deployed per accepted EVM chain. Addresses must be EVM (0x…). Tidepay's own take rate is injected as an extra recipient and your percentages are rescaled to fit. IMMUTABLE — the recipient list is baked into the contract address subscribers authorize against.","$ref":"#/components/schemas/CreatePlanSplitEvmRequest"},"splitSolana":{"description":"Split each Solana payout among several recipients. Solana has no split contract — the operator fans out one transfer per recipient at distribute time. Addresses must be Solana (base58). Same take-rate injection and immutability as splitEvm.","$ref":"#/components/schemas/CreatePlanSplitSolanaRequest"},"merchantDestinationWallet":{"description":"Payout address for this option. Falls back to your registered settlement wallet for the chosen token.","anyOf":[{"type":"string","pattern":"^0x[a-fA-F0-9]{40}$"},{"type":"string","pattern":"^[1-9A-HJ-NP-Za-km-z]{32,44}$"}]},"trialDays":{"examples":[7],"type":"integer","minimum":0,"maximum":365},"maxCycles":{"examples":[12],"type":"integer","exclusiveMinimum":0,"maximum":100000},"maxFailedCycles":{"examples":[3],"type":"integer","exclusiveMinimum":0,"maximum":1000}},"required":["amount","acceptedTokens","billing"],"additionalProperties":false},"UpsertWalletRequest":{"type":"object","properties":{"tokenKey":{"type":"string","enum":["usdc-polygon","usdt-polygon","usdc-arbitrum","usdt-arbitrum","usdc-base","eurc-base","usdc-optimism","usdt-optimism","usdc-base-sepolia","usdc-solana","usdt-solana","eurc-solana","usdc-solana-devnet"],"description":"Which chain+token this wallet settles, as a catalog key from GET /v1/currencies. One registered wallet per token — posting the same tokenKey again replaces the address.","examples":["usdc-polygon"]},"wallet":{"type":"string","description":"Your payout address for this token. Must match the token's chain family (0x… for EVM, base58 for Solana). Once registered it OVERRIDES the merchantDestinationWallet on every offer using this token, retroactively — including offers created before it existed. Deleting it reverts to each offer's own address.","examples":["0x71c7656ec7ab88b098defb751b7401b5f6d8976"]}},"required":["tokenKey","wallet"]},"CreateCustomerRequest":{"type":"object","properties":{"wallets":{"description":"The customer's paying wallets, at most one per chain family (evm, solana). A subscription only starts billing once a wallet matching its chosen chain family is present — but you can create the customer first and add wallets later via PATCH. Sending this REPLACES the whole list.","$ref":"#/components/schemas/Wallets"},"merchantReferenceId":{"description":"Your own id for this person, for reconciliation against your systems. Lives on the customer (not the subscription) because it identifies the payer, not one purchase.","examples":["cust-external-123"],"type":"string","maxLength":200},"email":{"description":"Contact email. Tidepay does not email your customers — this is stored for your own use and returned on responses and webhooks.","examples":["jane@example.com"],"type":"string","format":"email","pattern":"^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},"phone":{"description":"Phone number in E.164 format (leading +, country code). Normalized on save so the same number is never stored twice under different local formats.","$ref":"#/components/schemas/Phone"},"name":{"description":"The customer's full name.","examples":["Jane Doe"],"type":"string","maxLength":200},"dob":{"description":"Date of birth as YYYY-MM-DD. Date only — a birthdate has no time-of-day component.","examples":["1990-01-15"],"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"pob":{"description":"Place of birth, free text (city/country). No fixed taxonomy.","examples":["Austin, TX"],"type":"string","maxLength":200},"address":{"description":"Structured postal address. Stored as fields rather than one blob so it can be reused for invoicing and compliance.","$ref":"#/components/schemas/Address"},"idempotencyKey":{"description":"Your own key for this logical operation. Retrying with the same key returns the ORIGINAL customer with 200 instead of creating a duplicate. Scoped per merchant and per live/test mode.","examples":["550e8400-e29b-41d4-a716-446655440000"],"type":"string","minLength":1,"maxLength":200}}},"Wallets":{"maxItems":10,"type":"array","items":{"type":"object","properties":{"chainFamily":{"type":"string","description":"Which chain family this wallet belongs to. Currently \"evm\" or \"solana\" — treated as an open string rather than a fixed set, so a future family is a new accepted value here, not a breaking change to this field's type.","examples":["evm"]},"address":{"anyOf":[{"type":"string","pattern":"^0x[a-fA-F0-9]{40}$"},{"type":"string","pattern":"^[1-9A-HJ-NP-Za-km-z]{32,44}$"}]}},"required":["chainFamily","address"]}},"Phone":{"description":"Phone number in E.164 format: a leading '+', country code, then up to 14 digits. Spaces, dashes and parentheses are stripped before validation.","type":"string"},"Address":{"type":"object","properties":{"line1":{"type":"string","minLength":1,"maxLength":200,"examples":["123 Main St"]},"line2":{"examples":["Apt 4B"],"type":"string","maxLength":200},"city":{"type":"string","minLength":1,"maxLength":200,"examples":["Austin"]},"state":{"examples":["TX"],"type":"string","maxLength":200},"postalCode":{"type":"string","minLength":1,"maxLength":20,"examples":["78701"]},"country":{"type":"string","minLength":2,"maxLength":2,"examples":["US"]}},"required":["line1","city","postalCode","country"]},"UpdateCustomerRequest":{"type":"object","properties":{"wallets":{"description":"The customer's paying wallets, at most one per chain family (evm, solana). A subscription only starts billing once a wallet matching its chosen chain family is present — but you can create the customer first and add wallets later via PATCH. Sending this REPLACES the whole list.","$ref":"#/components/schemas/Wallets"},"merchantReferenceId":{"description":"Your own id for this person, for reconciliation against your systems. Lives on the customer (not the subscription) because it identifies the payer, not one purchase.","examples":["cust-external-123"],"type":"string","maxLength":200},"email":{"description":"Contact email. Tidepay does not email your customers — this is stored for your own use and returned on responses and webhooks.","examples":["jane@example.com"],"type":"string","format":"email","pattern":"^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},"phone":{"description":"Phone number in E.164 format (leading +, country code). Normalized on save so the same number is never stored twice under different local formats.","$ref":"#/components/schemas/Phone"},"name":{"description":"The customer's full name.","examples":["Jane Doe"],"type":"string","maxLength":200},"dob":{"description":"Date of birth as YYYY-MM-DD. Date only — a birthdate has no time-of-day component.","examples":["1990-01-15"],"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"pob":{"description":"Place of birth, free text (city/country). No fixed taxonomy.","examples":["Austin, TX"],"type":"string","maxLength":200},"address":{"description":"Structured postal address. Stored as fields rather than one blob so it can be reused for invoicing and compliance.","$ref":"#/components/schemas/Address"}}},"CreateSubscriptionRequest":{"type":"object","properties":{"offerId":{"type":"string","minLength":1,"description":"The offer the customer is subscribing to.","examples":["offer_01j8x2q3r4s5t6u7v8w9x0y1z2"]},"priceId":{"description":"Which pricing option on that offer to bill. Required when the offer has more than one active option — the choice is the customer's and is never guessed. Omit only when there is exactly one.","examples":["price_01j8x2q3r4s5t6u7v8w9x0y1z3"],"type":"string","minLength":1},"customerId":{"type":"string","minLength":1,"description":"The payer. Create them first with POST /v1/customers — billing only starts once they hold a wallet matching the chosen chain family.","examples":["cust_01j8x2q3r4s5t6u7v8w9x0y1z2"]},"billingCycleAnchor":{"description":"Explicit Unix timestamp or ISO-8601 string for the billing cycle anchor date.","examples":["2026-10-01T00:00:00.000Z"],"type":"string"},"billingCycleAnchorConfig":{"description":"Configuration object to automatically align monthly or annual renewal dates.","$ref":"#/components/schemas/BillingCycleAnchorConfig"},"prorationBehavior":{"description":"Proration behavior for period alignment ('none' | 'create_prorations').","examples":["create_prorations"],"type":"string","enum":["none","create_prorations"]},"customVariables":{"description":"Arbitrary key-value data echoed back untouched on every response and webhook for this subscription. Never interpreted by Tidepay. The only field editable after creation.","examples":[{"orderId":"12345"}],"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},"idempotencyKey":{"description":"Your own key for this logical operation. Retrying with the same key returns the ORIGINAL subscription with 200 instead of creating a second one — which makes it safe to retry after a timeout without knowing whether the first attempt landed. Scoped per merchant and per live/test mode.","examples":["550e8400-e29b-41d4-a716-446655440000"],"type":"string","minLength":1,"maxLength":200}},"required":["offerId","customerId"]},"BillingCycleAnchorConfig":{"type":"object","properties":{"dayOfMonth":{"description":"Day of month (1-31) on which to anchor future renewals.","examples":[31],"type":"integer","minimum":1,"maximum":31},"month":{"description":"Month of year (1-12) for annual anchors.","examples":[7],"type":"integer","minimum":1,"maximum":12},"hour":{"description":"UTC hour (0-23) for future renewals.","examples":[12],"type":"integer","minimum":0,"maximum":23},"minute":{"description":"UTC minute (0-59) for future renewals.","examples":[0],"type":"integer","minimum":0,"maximum":59},"second":{"description":"UTC second (0-59) for future renewals.","examples":[0],"type":"integer","minimum":0,"maximum":59}}},"UpdateSubscriptionRequest":{"type":"object","properties":{"customVariables":{"description":"Arbitrary key-value data echoed back untouched on every response and webhook for this subscription. Never interpreted by Tidepay. The only field editable after creation.","examples":[{"orderId":"12345"}],"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}},"MigrateSubscriptionRequest":{"type":"object","properties":{"priceId":{"type":"string","minLength":1,"description":"The pricing option to move this subscriber onto. Must belong to the SAME offer and must not be one-time. Billing does not change until the customer completes the returned authorizationUrl.","examples":["price_01j8x2q3r4s5t6u7v8w9x0y1z7"]}},"required":["priceId"],"additionalProperties":false},"CreatePaymentRequest":{"type":"object","properties":{"paymentMethod":{"default":"wallet_connect","description":"\"wallet_connect\" (default) pulls from `fromWallet`, which must already have granted the operator an allowance covering `amount`. \"wallet_transfer\" instead returns a dedicated deposit address for the payer to send to manually — no allowance, and `fromWallet` must be omitted.","type":"string","enum":["wallet_connect","wallet_transfer"]},"fromWallet":{"description":"The wallet funds are pulled from. Required for wallet_connect and must have an existing on-chain allowance; must be OMITTED for wallet_transfer, where the payer's wallet is unknown in advance.","anyOf":[{"type":"string","pattern":"^0x[a-fA-F0-9]{40}$"},{"type":"string","pattern":"^[1-9A-HJ-NP-Za-km-z]{32,44}$"}]},"toWallet":{"anyOf":[{"type":"string","pattern":"^0x[a-fA-F0-9]{40}$"},{"type":"string","pattern":"^[1-9A-HJ-NP-Za-km-z]{32,44}$"}],"description":"Where the funds are forwarded — your own wallet, or any third party you are paying out. Must match the chain family of `tokenKey`. Tidepay never takes custody; the operator only holds funds mid-transaction."},"amount":{"type":"string","pattern":"^\\d+(\\.\\d+)?$","description":"Decimal string in `tokenKey`'s own units — this is a direct token amount, not a fiat price, so no conversion happens. Minimum 1.00.","examples":["25.00"]},"tokenKey":{"type":"string","enum":["usdc-polygon","usdt-polygon","usdc-arbitrum","usdt-arbitrum","usdc-base","eurc-base","usdc-optimism","usdt-optimism","usdc-base-sepolia","usdc-solana","usdt-solana","eurc-solana","usdc-solana-devnet"],"description":"Which chain+token to move, as a catalog key from GET /v1/currencies.","examples":["usdc-polygon"]},"idempotencyKey":{"type":"string","minLength":1,"maxLength":200,"description":"REQUIRED here, unlike elsewhere: this endpoint moves money synchronously, so a blind retry without a key could double-pull. Reusing a key returns the original payment instead of charging again.","examples":["550e8400-e29b-41d4-a716-446655440000"]}},"required":["toWallet","amount","tokenKey","idempotencyKey"]},"UpdateWebhookEndpointRequest":{"type":"object","properties":{"url":{"anyOf":[{"type":"string","format":"uri"},{"type":"string","const":""}],"description":"Where signed webhooks for THIS environment are delivered. Send an empty string to unset it — for a test key that reverts sandbox events to the live endpoint; for a live key it stops live delivery entirely. Must be a public http(s) URL: private, loopback, and link-local addresses are rejected.","examples":["https://example.com/webhooks/tidepay"]}},"required":["url"],"additionalProperties":false},"ListCurrenciesResponse":{"type":"object","properties":{"chains":{"type":"array","items":{"$ref":"#/components/schemas/CurrencyChain"}}},"required":["chains"],"additionalProperties":false,"description":"The chain + token catalog. Testnets are excluded unless ?includeTestnets=true."},"CurrencyChain":{"type":"object","properties":{"chainId":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"examples":[137]},"family":{"type":"string","description":"Currently \"evm\" or \"solana\".","examples":["evm"]},"label":{"type":"string","examples":["Polygon"]},"testnet":{"type":"boolean"},"tokens":{"type":"array","items":{"$ref":"#/components/schemas/CurrencyToken"}}},"required":["chainId","family","label","testnet","tokens"],"additionalProperties":false},"CurrencyToken":{"type":"object","properties":{"key":{"type":"string","enum":["usdc-polygon","usdt-polygon","usdc-arbitrum","usdt-arbitrum","usdc-base","eurc-base","usdc-optimism","usdt-optimism","usdc-base-sepolia","usdc-solana","usdt-solana","eurc-solana","usdc-solana-devnet"]},"ticker":{"type":"string","enum":["USDC","USDT","EURC"]},"decimals":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"examples":[6]},"address":{"description":"EVM token contract address — present only when the parent chain's family is \"evm\".","type":"string"},"mint":{"description":"SPL mint address (base58) — present only when the parent chain's family is \"solana\".","type":"string"}},"required":["key","ticker","decimals"],"additionalProperties":false},"ErrorResponse":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Duplicates the HTTP status code, for consumers that only keep the response body.","examples":[400]},"type":{"$ref":"#/components/schemas/ErrorType"},"message":{"type":"string","description":"Human-readable detail. Not stable across releases — do not pattern-match on this.","examples":["amount must be a positive decimal string"]}},"required":["code","type","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false,"description":"The envelope returned by every non-2xx response from this API."},"ErrorType":{"type":"string","enum":["validation_error","authentication_error","forbidden","not_found","conflict","rate_limit_error","internal_error"],"description":"Stable, machine-readable error category. Prefer branching on this over parsing `message`."},"Offer":{"type":"object","properties":{"id":{"type":"string","examples":["offer_01j8x2q3r4s5t6u7v8w9x0y1z2"]},"name":{"type":"string","examples":["Pro"]},"description":{"type":["string","null"]},"imageUrl":{"type":["string","null"]},"identifier":{"description":"Merchant-chosen unique slug, for addressing an offer without storing its opaque id (GET /v1/offers?identifier=).","examples":["pro"],"type":["string","null"]},"productRef":{"examples":["SKU-1234"],"type":["string","null"]},"checkoutUrl":{"type":"string","description":"Computed, not a stored field — the shareable checkout URL. A capability of the offer, not a separate resource. When the offer has several pricing options, the customer picks one here.","examples":["https://tidepay.cc/pay/a8f3k2"]},"pricing":{"type":"array","items":{"$ref":"#/components/schemas/PricingOption"}},"active":{"type":"boolean"},"availableQuantity":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"description":"Total checkouts allowed, not remaining. Null means unlimited.","examples":[100]},"redeemedCount":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"examples":[12]},"expiresAt":{"anyOf":[{"type":"string","description":"ISO 8601 timestamp.","examples":["2026-08-19T14:32:00.000Z"]},{"type":"null"}]},"afterCompletion":{"anyOf":[{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},{"type":"null"}]},"features":{"type":"array","items":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},"description":"Descriptive metadata shown at checkout. Never affects billing."},"collectFields":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},"allowedPaymentMethods":{"anyOf":[{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},{"type":"null"}]},"metadata":{"anyOf":[{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},{"type":"null"}],"description":"Free-form key-value metadata, never interpreted by Tidepay.","examples":[{"orderId":"12345"}]},"reference":{"examples":["ORDER-2024-001"],"type":["string","null"]},"isSandbox":{"type":"boolean"},"createdAt":{"type":"string","description":"ISO 8601 timestamp.","examples":["2026-08-19T14:32:00.000Z"]},"updatedAt":{"type":"string","description":"ISO 8601 timestamp.","examples":["2026-08-19T14:32:00.000Z"]}},"required":["id","name","description","imageUrl","identifier","productRef","checkoutUrl","pricing","active","availableQuantity","redeemedCount","expiresAt","afterCompletion","features","collectFields","allowedPaymentMethods","metadata","reference","isSandbox","createdAt","updatedAt"],"additionalProperties":false,"description":"What a merchant sells, and on what terms. The single sellable resource — it replaces the separate Plan and Paylink resources, and carries its own pricing and checkout URL. Every non-delete offer endpoint returns this same full shape."},"PricingOption":{"type":"object","properties":{"id":{"type":"string","examples":["price_01j8x2q3r4s5t6u7v8w9x0y1z3"],"description":"Referenceable, but not manageable — a subscription records which option it was bought under, and reporting can group by it. There is deliberately no /v1/prices endpoint: pricing is changed by PATCHing the offer."},"label":{"examples":["Monthly"],"type":["string","null"]},"amount":{"type":"string","pattern":"^\\d+(\\.\\d+)?$","description":"Decimal string, denominated in `currency` — the shop-window price. When `currency` is a fiat code this is NOT a token amount; see acceptedTokens[].amountBaseUnits for what is actually charged.","examples":["30"]},"currency":{"type":"string","description":"Denomination of `amount`. A fiat code (USD, BRL, MXN, COP, ARS) is converted to each accepted token ONCE at creation and frozen; a stablecoin ticker (USDC, USDT) means `amount` is already the token amount.","examples":["USDC"]},"billing":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{},"description":"Either { type: \"one_time\" } or { type: \"recurring\", interval, intervalCount? }.","examples":[{"type":"recurring","interval":"month"}]},"acceptedTokens":{"type":"array","items":{"type":"object","properties":{"tokenKey":{"type":"string","enum":["usdc-polygon","usdt-polygon","usdc-arbitrum","usdt-arbitrum","usdc-base","eurc-base","usdc-optimism","usdt-optimism","usdc-base-sepolia","usdc-solana","usdt-solana","eurc-solana","usdc-solana-devnet"]},"chainId":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"examples":[8453]},"amountBaseUnits":{"description":"The amount actually charged, in this token's base units, frozen at creation. A subscriber pays this same number of tokens every cycle for the life of the subscription, whatever the exchange rate does afterwards.","examples":["30000000"],"type":["string","null"]}},"required":["tokenKey","chainId","amountBaseUnits"],"additionalProperties":false},"description":"Every chain+ticker combination this option accepts. The customer picks one at checkout."},"trialDays":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"examples":[7]},"maxCycles":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"examples":[12]},"maxFailedCycles":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"examples":[3]},"active":{"type":"boolean"}},"required":["id","label","amount","currency","billing","acceptedTokens","trialDays","maxCycles","maxFailedCycles","active"],"additionalProperties":false,"description":"One set of billing terms on an offer. Terms are IMMUTABLE once created — an EVM subscriber's allowance is sized against the amount in force when they subscribed, and Solana's subscriptions program stores it in an immutable on-chain account. Add an option, or retire one; never rewrite one."},"ListOffersResponse":{"type":"object","properties":{"offers":{"type":"array","items":{"$ref":"#/components/schemas/Offer"}},"limit":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"examples":[20]},"start":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"examples":[0]}},"required":["offers","limit","start"],"additionalProperties":false},"OfferDeleted":{"type":"object","properties":{"id":{"type":"string"},"deleted":{"type":"boolean","const":true}},"required":["id","deleted"],"additionalProperties":false},"PricingOptionRetired":{"type":"object","properties":{"id":{"type":"string"},"deleted":{"type":"boolean","description":"True when the option was removed outright (nothing had ever subscribed to it); false when it was deactivated instead, to keep existing subscriptions' priceId reference resolvable."},"active":{"type":"boolean"},"hasActiveSubscriptions":{"type":"boolean"}},"required":["id","deleted"],"additionalProperties":false},"MerchantWallet":{"type":"object","properties":{"id":{"type":"string","examples":["wal_01j8x2q3r4s5t6u7v8w9x0y1z2"]},"merchantId":{"type":"string"},"tokenKey":{"type":"string","enum":["usdc-polygon","usdt-polygon","usdc-arbitrum","usdt-arbitrum","usdc-base","eurc-base","usdc-optimism","usdt-optimism","usdc-base-sepolia","usdc-solana","usdt-solana","eurc-solana","usdc-solana-devnet"]},"chainId":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"examples":[137]},"receiverWallet":{"type":"string","description":"Empty string means registered but not yet connected — see POST /wallets.","examples":["0x71c7656ec7ab88b098defb751b7401b5f6d8976"]},"createdAt":{"type":"string","description":"ISO 8601 timestamp.","examples":["2026-08-19T14:32:00.000Z"]}},"required":["id","merchantId","tokenKey","chainId","receiverWallet","createdAt"],"additionalProperties":false,"description":"A merchant's registered settlement wallet for one token. When present and non-empty, this wallet is used as the payout destination for any offer involving this tokenKey, taking priority over that request's own `merchantDestinationWallet`."},"ListWalletsResponse":{"type":"object","properties":{"wallets":{"type":"array","items":{"$ref":"#/components/schemas/MerchantWallet"}}},"required":["wallets"],"additionalProperties":false},"WalletDeleted":{"type":"object","properties":{"id":{"type":"string"},"deleted":{"type":"boolean","const":true}},"required":["id","deleted"],"additionalProperties":false},"CustomerFull":{"type":"object","properties":{"id":{"type":"string","examples":["cust_01j8x2q3r4s5t6u7v8w9x0y1z2"]},"wallets":{"type":"array","items":{"$ref":"#/components/schemas/Wallet"},"description":"One entry per chain family the customer has connected a wallet for (at most one EVM and one Solana entry today)."},"merchantReferenceId":{"description":"Your own internal id for this customer, if you set one.","type":["string","null"]},"email":{"type":["string","null"]},"phone":{"description":"E.164 format, e.g. +5511998887766.","type":["string","null"]},"name":{"type":["string","null"]},"createdAt":{"type":"string","description":"ISO 8601 timestamp.","examples":["2026-08-19T14:32:00.000Z"]},"updatedAt":{"type":"string","description":"ISO 8601 timestamp.","examples":["2026-08-19T14:32:00.000Z"]},"merchantId":{"type":"string"},"isSandbox":{"type":"boolean"},"idempotencyKey":{"type":["string","null"]}},"required":["id","wallets","merchantReferenceId","email","phone","name","createdAt","updatedAt","merchantId","isSandbox","idempotencyKey"],"additionalProperties":false,"description":"The shape returned by `POST /customers`, `PATCH /customers/{customerId}`, and `GET /customers` (list) — a superset of the `Customer` object returned by `GET /customers/{customerId}`, which omits `merchantId`, `isSandbox`, and `idempotencyKey`."},"Wallet":{"type":"object","properties":{"chainFamily":{"type":"string","description":"Currently \"evm\" or \"solana\".","examples":["evm"]},"address":{"type":"string","description":"An EVM address (0x…, lowercased) or a Solana address (base58).","examples":["0x71c7656ec7ab88b098defb751b7401b5f6d8976"]}},"required":["chainFamily","address"],"additionalProperties":false},"ListCustomersResponse":{"type":"object","properties":{"customers":{"type":"array","items":{"$ref":"#/components/schemas/CustomerFull"}},"limit":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"examples":[20]},"start":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"examples":[0]}},"required":["customers","limit","start"],"additionalProperties":false},"Customer":{"type":"object","properties":{"id":{"type":"string","examples":["cust_01j8x2q3r4s5t6u7v8w9x0y1z2"]},"wallets":{"type":"array","items":{"$ref":"#/components/schemas/Wallet"},"description":"One entry per chain family the customer has connected a wallet for (at most one EVM and one Solana entry today)."},"merchantReferenceId":{"description":"Your own internal id for this customer, if you set one.","type":["string","null"]},"email":{"type":["string","null"]},"phone":{"description":"E.164 format, e.g. +5511998887766.","type":["string","null"]},"name":{"type":["string","null"]},"createdAt":{"type":"string","description":"ISO 8601 timestamp.","examples":["2026-08-19T14:32:00.000Z"]},"updatedAt":{"type":"string","description":"ISO 8601 timestamp.","examples":["2026-08-19T14:32:00.000Z"]}},"required":["id","wallets","merchantReferenceId","email","phone","name","createdAt","updatedAt"],"additionalProperties":false,"description":"The payer — the wallet (and, later, other payment credentials) that funds a subscription. This is the shape returned by `GET /customers/{customerId}` specifically."},"CustomerDeleted":{"type":"object","properties":{"id":{"type":"string"},"deleted":{"type":"boolean","const":true}},"required":["id","deleted"],"additionalProperties":false},"CreateSubscriptionResponse":{"anyOf":[{"type":"object","properties":{"id":{"type":"string","examples":["sub_01j8x2q3r4s5t6u7v8w9x0y1z2"]},"status":{"type":"string","enum":["pending","active","past_due","canceled"],"description":"Always \"pending\" immediately after creation, unless this response is an idempotency replay of an already-progressed subscription."},"subscribeUrl":{"type":"string","description":"Redirect the subscriber here to connect their wallet and grant the on-chain allowance.","examples":["https://tidepay.cc/subscribe/sub_01j8x2q3r4s5t6u7v8w9x0y1z2"]},"requiredAllowance":{"type":"string","description":"The exact on-chain amount (in the token's smallest unit) the subscriber authorizes — 12 billing cycles' worth of the plan's price in this token. That price was fixed when the plan was created, so every cycle costs the same and no exchange-rate buffer is added. On Solana the subscriber subscribes to the on-chain plan instead of granting an allowance, and the program caps each pull at one period's amount regardless of this figure."},"operatorWallet":{"type":"string","description":"The Tidepay operator address that will execute pulls. A fixed sandbox placeholder when created with a test API key.","examples":["0x71c7656ec7ab88b098defb751b7401b5f6d8976"]},"tokenAddress":{"description":"Present only for EVM plans (mutually exclusive with mint).","type":"string"},"mint":{"description":"Present only for Solana plans (mutually exclusive with tokenAddress).","type":"string"},"chainId":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"examples":[137]}},"required":["id","status","subscribeUrl","requiredAllowance","operatorWallet","chainId"],"additionalProperties":false},{"type":"object","properties":{"id":{"type":"string","examples":["sub_01j8x2q3r4s5t6u7v8w9x0y1z2"]},"status":{"type":"string","enum":["pending","active","past_due","canceled"]},"subscribeUrl":{"type":"string","description":"Redirect the subscriber here — the chain-selection step runs before wallet-connect for a plan accepting more than one chain.","examples":["https://tidepay.cc/subscribe/sub_01j8x2q3r4s5t6u7v8w9x0y1z2"]},"needsChainSelection":{"type":"boolean","const":true,"description":"The plan accepts more than one chain and the subscriber hasn't picked one yet — none of requiredAllowance/operatorWallet/tokenAddress/mint/chainId can be computed until they do."},"acceptedChains":{"type":"array","items":{"type":"object","properties":{"chainId":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"examples":[137]},"tokenKey":{"type":"string","enum":["usdc-polygon","usdt-polygon","usdc-arbitrum","usdt-arbitrum","usdc-base","eurc-base","usdc-optimism","usdt-optimism","usdc-base-sepolia","usdc-solana","usdt-solana","eurc-solana","usdc-solana-devnet"]}},"required":["chainId","tokenKey"],"additionalProperties":false}}},"required":["id","status","subscribeUrl","needsChainSelection","acceptedChains"],"additionalProperties":false}],"description":"Returned by POST /subscriptions. Not the same shape as GET /subscriptions/{subscriptionId} — see SubscriptionDetail. Two possible shapes: the plan's chain was already resolved (single-chain plan, the default), or the plan accepts more than one chain and needsChainSelection is true — see acceptedChains for the set the subscriber can choose from at checkout."},"ListSubscriptionsResponse":{"type":"object","properties":{"subscriptions":{"type":"array","items":{"$ref":"#/components/schemas/SubscriptionListItem"}},"limit":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"examples":[20]},"start":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"examples":[0]}},"required":["subscriptions","limit","start"],"additionalProperties":false},"SubscriptionListItem":{"type":"object","properties":{"id":{"type":"string"},"merchantId":{"type":"string"},"offerId":{"type":["string","null"]},"offerPriceId":{"description":"Which pricing option on the offer this subscription bills. Null once that option has been deleted — the snapshot fields below still record what was agreed to.","type":["string","null"]},"offerName":{"type":"string","description":"Snapshot of the offer's name when the subscription was created. Survives the offer being renamed or deleted."},"priceAmount":{"type":"string","pattern":"^\\d+(\\.\\d+)?$","description":"Snapshot of the agreed price. Never re-read from the live offer, so editing an offer's pricing cannot reprice an existing subscriber.","examples":["30"]},"priceCurrency":{"type":"string","examples":["USDC"]},"customerId":{"type":"string"},"chainId":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"description":"The chain the subscriber chose (or the pricing option's only chain, when it accepts one). Null until a subscriber against a multi-chain option makes their choice at checkout."},"status":{"type":"string","enum":["pending","active","past_due","canceled"]},"currentPeriodEnd":{"anyOf":[{"type":"string","description":"ISO 8601 timestamp.","examples":["2026-08-19T14:32:00.000Z"]},{"type":"null"}]},"allowanceConfirmed":{"type":"boolean"},"cyclesCompleted":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"examples":[3]},"consecutiveFailures":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"examples":[0]},"customVariables":{"anyOf":[{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},{"type":"null"}]},"afterCompletion":{"anyOf":[{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},{"type":"null"}]},"isSandbox":{"type":"boolean"},"idempotencyKey":{"type":["string","null"]},"affiliatePartnerId":{"type":["string","null"]},"affiliateCommissionPct":{"type":["string","null"]},"createdAt":{"type":"string","description":"ISO 8601 timestamp.","examples":["2026-08-19T14:32:00.000Z"]},"updatedAt":{"type":"string","description":"ISO 8601 timestamp.","examples":["2026-08-19T14:32:00.000Z"]}},"required":["id","merchantId","offerId","offerPriceId","offerName","priceAmount","priceCurrency","customerId","chainId","status","currentPeriodEnd","allowanceConfirmed","cyclesCompleted","consecutiveFailures","customVariables","afterCompletion","isSandbox","idempotencyKey","affiliatePartnerId","affiliateCommissionPct","createdAt","updatedAt"],"additionalProperties":false,"description":"The shape of each item in `GET /subscriptions` (list). Wider than `SubscriptionDetail` — includes every underlying column — but does not include `lastInvoice`."},"SubscriptionDetail":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string","enum":["pending","active","past_due","canceled"]},"currentPeriodEnd":{"anyOf":[{"type":"string","description":"ISO 8601 timestamp.","examples":["2026-08-19T14:32:00.000Z"]},{"type":"null"}]},"allowanceConfirmed":{"type":"boolean"},"customerId":{"type":"string"},"cyclesCompleted":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"examples":[3]},"consecutiveFailures":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"examples":[0]},"customVariables":{"anyOf":[{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},{"type":"null"}]},"lastInvoice":{"anyOf":[{"$ref":"#/components/schemas/Invoice"},{"type":"null"}],"description":"The subscription's most recent invoice attempt (any status), or null if it has never been billed yet."}},"required":["id","status","currentPeriodEnd","allowanceConfirmed","customerId","cyclesCompleted","consecutiveFailures","customVariables","lastInvoice"],"additionalProperties":false,"description":"The shape returned by `GET /subscriptions/{subscriptionId}` specifically. This is a curated subset of the underlying subscription record plus `lastInvoice` — it omits `merchantId`, `offerId`, `chainId`, `isSandbox`, `afterCompletion`, `idempotencyKey`, `affiliatePartnerId`, `affiliateCommissionPct`, `createdAt`, `updatedAt`, all of which exist in the database but are not part of this response. `GET /subscriptions` (list) returns a different, wider shape instead — see `SubscriptionListItem`."},"Invoice":{"type":"object","properties":{"id":{"type":"string","examples":["inv_01j8x2q3r4s5t6u7v8w9x0y1z2"]},"subscriptionId":{"type":"string"},"merchantId":{"type":"string"},"idempotencyKey":{"type":"string"},"grossAmount":{"type":"string","pattern":"^\\d+(\\.\\d+)?$","description":"Positive decimal string. Minimum plan amount is 1.00 USD (or currency equivalent).","examples":["10.00"]},"platformAmount":{"type":"string","pattern":"^\\d+(\\.\\d+)?$","description":"Positive decimal string. Minimum plan amount is 1.00 USD (or currency equivalent).","examples":["10.00"]},"merchantAmount":{"type":"string","pattern":"^\\d+(\\.\\d+)?$","description":"Positive decimal string. Minimum plan amount is 1.00 USD (or currency equivalent).","examples":["10.00"]},"tokenKey":{"type":"string","enum":["usdc-polygon","usdt-polygon","usdc-arbitrum","usdt-arbitrum","usdc-base","eurc-base","usdc-optimism","usdt-optimism","usdc-base-sepolia","usdc-solana","usdt-solana","eurc-solana","usdc-solana-devnet"]},"chainId":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"examples":[137]},"pullTxHash":{"type":["string","null"]},"merchantTransferTxHash":{"type":["string","null"]},"platformTransferTxHash":{"type":["string","null"]},"distributeTxHash":{"type":["string","null"]},"status":{"type":"string","enum":["pending","pulled","settled","failed"]},"failureReason":{"examples":["insufficient_allowance"],"type":["string","null"]},"isSandbox":{"type":"boolean"},"createdAt":{"type":"string","description":"ISO 8601 timestamp.","examples":["2026-08-19T14:32:00.000Z"]},"updatedAt":{"type":"string","description":"ISO 8601 timestamp.","examples":["2026-08-19T14:32:00.000Z"]}},"required":["id","subscriptionId","merchantId","idempotencyKey","grossAmount","platformAmount","merchantAmount","tokenKey","chainId","pullTxHash","merchantTransferTxHash","platformTransferTxHash","distributeTxHash","status","failureReason","isSandbox","createdAt","updatedAt"],"additionalProperties":false,"description":"One row per subscription billing-cycle attempt — the source of truth for recurring revenue. This is the shape returned as `lastInvoice` on `GET /subscriptions/{subscriptionId}`."},"SubscriptionCanceled":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string","const":"canceled"}},"required":["id","status"],"additionalProperties":false},"SubscriptionResumed":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string","enum":["active","pending_authorization"]},"reauthorizationRequired":{"type":"boolean","description":"False when the subscription reactivated outright (it was past_due, and its on-chain authorization still stands). True when the customer must re-authorize before billing resumes — `authorizationUrl` is then present."},"authorizationUrl":{"description":"Where to send the customer to re-authorize. Present only when reauthorizationRequired is true.","examples":["https://tidepay.cc/subscribe/sub_01j8x2q3r4s5t6u7v8w9x0y1z6"],"type":"string"}},"required":["id","status","reauthorizationRequired"],"additionalProperties":false},"SubscriptionMigrating":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string","const":"pending_authorization"},"priceId":{"description":"Still the CURRENT option — billing has not moved yet and will not until the customer authorizes.","type":["string","null"]},"pendingOfferPriceId":{"type":"string"},"pendingPriceId":{"type":"string","description":"The option the subscription will move to once authorized."},"authorizationUrl":{"type":"string","examples":["https://tidepay.cc/subscribe/sub_01j8x2q3r4s5t6u7v8w9x0y1z6"]}},"required":["id","status","priceId","pendingPriceId","authorizationUrl"],"additionalProperties":false},"SubscriptionRetryResponse":{"type":"object","properties":{"subscriptionId":{"type":"string"},"outcome":{"type":"string","enum":["settled","failed","skipped"]},"status":{"type":"string","enum":["pending","active","past_due","canceled"],"description":"Caution: this reflects the subscription's status as read BEFORE the retry ran, not re-fetched afterward — it can be stale relative to `outcome` (e.g. still show \"past_due\" even though the retry just settled and flipped it to \"active\"). Re-fetch GET /subscriptions/{subscriptionId} for the current status."}},"required":["subscriptionId","outcome","status"],"additionalProperties":false},"Payment":{"type":"object","properties":{"id":{"type":"string","examples":["pay_01j8x2q3r4s5t6u7v8w9x0y1z2"]},"merchantId":{"type":"string"},"customerId":{"type":["string","null"]},"source":{"type":"string","enum":["one_time","subscription"],"description":"Which flow produced this payment. \"one_time\" is a direct charge or a one-time offer checkout; \"subscription\" is a recurring billing cycle. Both are listed by GET /v1/payments — there is no separate invoices resource."},"subscriptionId":{"description":"Set when `source` is \"subscription\" — the subscription this cycle billed. Null for one-off payments.","type":["string","null"]},"offerId":{"description":"The offer this payment was bought from. Null for merchant-initiated POST /v1/payments, which names an amount directly rather than buying a published price.","type":["string","null"]},"priceId":{"description":"Which pricing option was charged.","type":["string","null"]},"periodStart":{"anyOf":[{"type":"string","description":"ISO 8601 timestamp.","examples":["2026-08-19T14:32:00.000Z"]},{"type":"null"}],"description":"Billing period bounds. Null unless `source` is \"subscription\"."},"periodEnd":{"anyOf":[{"type":"string","description":"ISO 8601 timestamp.","examples":["2026-08-19T14:32:00.000Z"]},{"type":"null"}]},"attempts":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"description":"How many times this cycle has been attempted. Null unless `source` is \"subscription\".","examples":[1]},"idempotencyKey":{"type":"string"},"paymentMethod":{"type":"string","enum":["wallet_connect","wallet_transfer"]},"fromWallet":{"anyOf":[{"type":"string","description":"An EVM address (0x…, lowercased) or a Solana address (base58).","examples":["0x71c7656ec7ab88b098defb751b7401b5f6d8976"]},{"type":"null"}],"description":"Null for wallet_transfer payments — there is no pre-granted allowance to pull from."},"toWallet":{"type":"string","description":"An EVM address (0x…, lowercased) or a Solana address (base58).","examples":["0x71c7656ec7ab88b098defb751b7401b5f6d8976"]},"amount":{"type":"string","pattern":"^\\d+(\\.\\d+)?$","description":"Positive decimal string. Minimum plan amount is 1.00 USD (or currency equivalent).","examples":["10.00"]},"platformAmount":{"anyOf":[{"type":"string","pattern":"^\\d+(\\.\\d+)?$","description":"Positive decimal string. Minimum plan amount is 1.00 USD (or currency equivalent).","examples":["10.00"]},{"type":"null"}],"description":"Null for merchant-initiated one-off payments — the platform take rate only applies to offer-driven payments."},"merchantAmount":{"anyOf":[{"type":"string","pattern":"^\\d+(\\.\\d+)?$","description":"Positive decimal string. Minimum plan amount is 1.00 USD (or currency equivalent).","examples":["10.00"]},{"type":"null"}]},"tokenKey":{"type":"string","enum":["usdc-polygon","usdt-polygon","usdc-arbitrum","usdt-arbitrum","usdc-base","eurc-base","usdc-optimism","usdt-optimism","usdc-base-sepolia","usdc-solana","usdt-solana","eurc-solana","usdc-solana-devnet"]},"chainId":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"examples":[137]},"pullTxHash":{"type":["string","null"]},"forwardTxHash":{"type":["string","null"]},"platformTransferTxHash":{"type":["string","null"]},"distributeTxHash":{"type":["string","null"]},"status":{"type":"string","enum":["pending","pulled","settled","failed","canceled"],"description":"By the time POST /payments responds, this already reflects the final settled/failed outcome for wallet_connect payments — the pull runs synchronously before the response is built. wallet_transfer payments stay pending until the payer sends funds to the returned deposit address. \"canceled\" is only reachable via POST /payments/{paymentId}/cancel, and only while still \"pending\"."},"failureReason":{"examples":["insufficient_allowance"],"type":["string","null"]},"isSandbox":{"type":"boolean"},"affiliatePartnerId":{"type":["string","null"]},"affiliateCommissionPct":{"type":["string","null"]},"createdAt":{"type":"string","description":"ISO 8601 timestamp.","examples":["2026-08-19T14:32:00.000Z"]},"updatedAt":{"type":"string","description":"ISO 8601 timestamp.","examples":["2026-08-19T14:32:00.000Z"]}},"required":["id","merchantId","customerId","source","subscriptionId","offerId","priceId","periodStart","periodEnd","attempts","idempotencyKey","paymentMethod","fromWallet","toWallet","amount","platformAmount","merchantAmount","tokenKey","chainId","pullTxHash","forwardTxHash","platformTransferTxHash","distributeTxHash","status","failureReason","isSandbox","affiliatePartnerId","affiliateCommissionPct","createdAt","updatedAt"],"additionalProperties":false,"description":"A one-off pull, outside the recurring plan/subscription machinery. This is the shape returned by `GET /payments/{paymentId}`, `GET /payments` (list), and `POST /payments` for wallet_connect payments. **Note:** for a `wallet_transfer` payment, `depositAddress`/`depositExpiresAt` are only ever included on the immediate `POST /payments` response (see `PaymentWalletTransfer` below) — a client polling `GET /payments/{paymentId}` afterward never sees those two fields again."},"PaymentWalletTransfer":{"type":"object","properties":{"id":{"type":"string","examples":["pay_01j8x2q3r4s5t6u7v8w9x0y1z2"]},"merchantId":{"type":"string"},"customerId":{"type":["string","null"]},"source":{"type":"string","enum":["one_time","subscription"],"description":"Which flow produced this payment. \"one_time\" is a direct charge or a one-time offer checkout; \"subscription\" is a recurring billing cycle. Both are listed by GET /v1/payments — there is no separate invoices resource."},"subscriptionId":{"description":"Set when `source` is \"subscription\" — the subscription this cycle billed. Null for one-off payments.","type":["string","null"]},"offerId":{"description":"The offer this payment was bought from. Null for merchant-initiated POST /v1/payments, which names an amount directly rather than buying a published price.","type":["string","null"]},"priceId":{"description":"Which pricing option was charged.","type":["string","null"]},"periodStart":{"anyOf":[{"type":"string","description":"ISO 8601 timestamp.","examples":["2026-08-19T14:32:00.000Z"]},{"type":"null"}],"description":"Billing period bounds. Null unless `source` is \"subscription\"."},"periodEnd":{"anyOf":[{"type":"string","description":"ISO 8601 timestamp.","examples":["2026-08-19T14:32:00.000Z"]},{"type":"null"}]},"attempts":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"description":"How many times this cycle has been attempted. Null unless `source` is \"subscription\".","examples":[1]},"idempotencyKey":{"type":"string"},"paymentMethod":{"type":"string","enum":["wallet_connect","wallet_transfer"]},"fromWallet":{"anyOf":[{"type":"string","description":"An EVM address (0x…, lowercased) or a Solana address (base58).","examples":["0x71c7656ec7ab88b098defb751b7401b5f6d8976"]},{"type":"null"}],"description":"Null for wallet_transfer payments — there is no pre-granted allowance to pull from."},"toWallet":{"type":"string","description":"An EVM address (0x…, lowercased) or a Solana address (base58).","examples":["0x71c7656ec7ab88b098defb751b7401b5f6d8976"]},"amount":{"type":"string","pattern":"^\\d+(\\.\\d+)?$","description":"Positive decimal string. Minimum plan amount is 1.00 USD (or currency equivalent).","examples":["10.00"]},"platformAmount":{"anyOf":[{"type":"string","pattern":"^\\d+(\\.\\d+)?$","description":"Positive decimal string. Minimum plan amount is 1.00 USD (or currency equivalent).","examples":["10.00"]},{"type":"null"}],"description":"Null for merchant-initiated one-off payments — the platform take rate only applies to offer-driven payments."},"merchantAmount":{"anyOf":[{"type":"string","pattern":"^\\d+(\\.\\d+)?$","description":"Positive decimal string. Minimum plan amount is 1.00 USD (or currency equivalent).","examples":["10.00"]},{"type":"null"}]},"tokenKey":{"type":"string","enum":["usdc-polygon","usdt-polygon","usdc-arbitrum","usdt-arbitrum","usdc-base","eurc-base","usdc-optimism","usdt-optimism","usdc-base-sepolia","usdc-solana","usdt-solana","eurc-solana","usdc-solana-devnet"]},"chainId":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"examples":[137]},"pullTxHash":{"type":["string","null"]},"forwardTxHash":{"type":["string","null"]},"platformTransferTxHash":{"type":["string","null"]},"distributeTxHash":{"type":["string","null"]},"status":{"type":"string","enum":["pending","pulled","settled","failed","canceled"],"description":"By the time POST /payments responds, this already reflects the final settled/failed outcome for wallet_connect payments — the pull runs synchronously before the response is built. wallet_transfer payments stay pending until the payer sends funds to the returned deposit address. \"canceled\" is only reachable via POST /payments/{paymentId}/cancel, and only while still \"pending\"."},"failureReason":{"examples":["insufficient_allowance"],"type":["string","null"]},"isSandbox":{"type":"boolean"},"affiliatePartnerId":{"type":["string","null"]},"affiliateCommissionPct":{"type":["string","null"]},"createdAt":{"type":"string","description":"ISO 8601 timestamp.","examples":["2026-08-19T14:32:00.000Z"]},"updatedAt":{"type":"string","description":"ISO 8601 timestamp.","examples":["2026-08-19T14:32:00.000Z"]},"depositAddress":{"description":"Where the payer must manually send funds. Only present on the POST /payments response for wallet_transfer payments.","type":["string","null"]},"depositExpiresAt":{"anyOf":[{"type":"string","description":"ISO 8601 timestamp.","examples":["2026-08-19T14:32:00.000Z"]},{"type":"null"}]}},"required":["id","merchantId","customerId","source","subscriptionId","offerId","priceId","periodStart","periodEnd","attempts","idempotencyKey","paymentMethod","fromWallet","toWallet","amount","platformAmount","merchantAmount","tokenKey","chainId","pullTxHash","forwardTxHash","platformTransferTxHash","distributeTxHash","status","failureReason","isSandbox","affiliatePartnerId","affiliateCommissionPct","createdAt","updatedAt","depositAddress","depositExpiresAt"],"additionalProperties":false,"description":"The shape `POST /payments` returns specifically for `paymentMethod: \"wallet_transfer\"` — adds `depositAddress`/`depositExpiresAt` on top of the base Payment object."},"ListPaymentsResponse":{"type":"object","properties":{"payments":{"type":"array","items":{"$ref":"#/components/schemas/Payment"}},"limit":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"examples":[20]},"start":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"examples":[0]}},"required":["payments","limit","start"],"additionalProperties":false},"WebhookEndpoint":{"type":"object","properties":{"environment":{"type":"string","enum":["live","test"],"description":"Which environment this endpoint serves — decided by the API key you authenticated with, not by the request body."},"url":{"description":"The endpoint configured for this environment, or null if none is set.","type":["string","null"]},"effectiveUrl":{"description":"Where events for this environment are actually delivered. Differs from `url` only for a test key with no sandbox endpoint set, where sandbox events fall back to the live endpoint.","type":["string","null"]}},"required":["environment","url","effectiveUrl"],"additionalProperties":false},"WebhookSecretStatus":{"type":"object","properties":{"configured":{"type":"boolean","description":"Whether a webhook signing secret has been generated yet."},"webhookUrl":{"description":"Endpoint receiving live events.","type":["string","null"]},"webhookUrlTest":{"description":"Endpoint receiving sandbox events. When null, sandbox events are delivered to webhookUrl instead.","type":["string","null"]}},"required":["configured","webhookUrl","webhookUrlTest"],"additionalProperties":false},"WebhookSecretRotated":{"type":"object","properties":{"webhookSecret":{"type":"string","description":"The new signing secret, in plaintext. Shown only this once — store it now, it cannot be retrieved again."}},"required":["webhookSecret"],"additionalProperties":false},"SandboxAdvanceResponse":{"type":"object","properties":{"cycles":{"type":"array","items":{"type":"string","enum":["settled","failed","skipped"]},"examples":[["settled","settled","failed"]]},"subscription":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string"},"currentPeriodEnd":{"type":["string","null"]}},"required":["id","status","currentPeriodEnd"],"additionalProperties":false}},"required":["cycles","subscription"],"additionalProperties":false,"description":"One outcome per billing cycle run, stopping at the first that did not settle, plus the subscription's state afterwards."},"SandboxChargeResponse":{"type":"object","properties":{"outcome":{"type":"string","enum":["settled","failed","skipped"]}},"required":["outcome"],"additionalProperties":false,"description":"The outcome of the force-run invoice cycle."},"CustomerCreatedWebhook":{"type":"object","properties":{"id":{"type":"string","description":"Server-generated event id, unique per delivery attempt's underlying event.","example":"evt_01j8x2q3r4s5t6u7v8w9x0y1z2"},"event":{"type":"string","const":"customer.created"},"createdAt":{"type":"string","description":"ISO 8601 timestamp of when the event was generated."},"isSandbox":{"type":"boolean","description":"true if this event was generated by sandbox (test-key) activity — route accordingly if you share one endpoint for both."},"data":{"type":"object","properties":{"customerId":{"type":"string"},"wallets":{"type":"array","items":{"type":"object","properties":{"chainFamily":{"type":"string","description":"Currently \"evm\" or \"solana\".","examples":["evm"]},"address":{"type":"string"}},"required":["chainFamily","address"],"additionalProperties":false}}},"required":["customerId","wallets"],"additionalProperties":false}},"required":["id","event","createdAt","isSandbox","data"],"additionalProperties":false,"description":"Fired after POST /customers creates a customer."},"CustomerUpdatedWebhook":{"type":"object","properties":{"id":{"type":"string","description":"Server-generated event id, unique per delivery attempt's underlying event.","example":"evt_01j8x2q3r4s5t6u7v8w9x0y1z2"},"event":{"type":"string","const":"customer.updated"},"createdAt":{"type":"string","description":"ISO 8601 timestamp of when the event was generated."},"isSandbox":{"type":"boolean","description":"true if this event was generated by sandbox (test-key) activity — route accordingly if you share one endpoint for both."},"data":{"type":"object","properties":{"customerId":{"type":"string"},"wallets":{"type":"array","items":{"type":"object","properties":{"chainFamily":{"type":"string","description":"Currently \"evm\" or \"solana\".","examples":["evm"]},"address":{"type":"string"}},"required":["chainFamily","address"],"additionalProperties":false}}},"required":["customerId","wallets"],"additionalProperties":false}},"required":["id","event","createdAt","isSandbox","data"],"additionalProperties":false,"description":"Fired after PATCH /customers/{customerId} updates a customer."},"CustomerDeletedWebhook":{"type":"object","properties":{"id":{"type":"string","description":"Server-generated event id, unique per delivery attempt's underlying event.","example":"evt_01j8x2q3r4s5t6u7v8w9x0y1z2"},"event":{"type":"string","const":"customer.deleted"},"createdAt":{"type":"string","description":"ISO 8601 timestamp of when the event was generated."},"isSandbox":{"type":"boolean","description":"true if this event was generated by sandbox (test-key) activity — route accordingly if you share one endpoint for both."},"data":{"type":"object","properties":{"customerId":{"type":"string"}},"required":["customerId"],"additionalProperties":false}},"required":["id","event","createdAt","isSandbox","data"],"additionalProperties":false,"description":"Fired after DELETE /customers/{customerId}. No walletAddress — the row is already gone by delivery time."},"SubscriptionCreatedWebhook":{"type":"object","properties":{"id":{"type":"string","description":"Server-generated event id, unique per delivery attempt's underlying event.","example":"evt_01j8x2q3r4s5t6u7v8w9x0y1z2"},"event":{"type":"string","const":"subscription.created"},"createdAt":{"type":"string","description":"ISO 8601 timestamp of when the event was generated."},"isSandbox":{"type":"boolean","description":"true if this event was generated by sandbox (test-key) activity — route accordingly if you share one endpoint for both."},"data":{"type":"object","properties":{"subscriptionId":{"type":"string"},"offerId":{"type":"string"},"priceId":{"type":"string"},"status":{"type":"string","const":"pending","description":"Always \"pending\" — this fires immediately on creation, before any allowance is granted."}},"required":["subscriptionId","offerId","priceId","status"],"additionalProperties":false}},"required":["id","event","createdAt","isSandbox","data"],"additionalProperties":false,"description":"Fired after POST /subscriptions creates a subscription. Not fired on an idempotency replay of an existing subscription."},"SubscriptionActiveWebhook":{"type":"object","properties":{"id":{"type":"string","description":"Server-generated event id, unique per delivery attempt's underlying event.","example":"evt_01j8x2q3r4s5t6u7v8w9x0y1z2"},"event":{"type":"string","const":"subscription.active"},"createdAt":{"type":"string","description":"ISO 8601 timestamp of when the event was generated."},"isSandbox":{"type":"boolean","description":"true if this event was generated by sandbox (test-key) activity — route accordingly if you share one endpoint for both."},"data":{"type":"object","properties":{"subscriptionId":{"type":"string"},"offerId":{"type":["string","null"]},"priceId":{"type":["string","null"]}},"required":["subscriptionId","offerId","priceId"],"additionalProperties":false}},"required":["id","event","createdAt","isSandbox","data"],"additionalProperties":false,"description":"Fired the first time a subscription's invoice cycle settles successfully — the transition from \"pending\" to \"active\"."},"SubscriptionCanceledWebhook":{"type":"object","properties":{"id":{"type":"string","description":"Server-generated event id, unique per delivery attempt's underlying event.","example":"evt_01j8x2q3r4s5t6u7v8w9x0y1z2"},"event":{"type":"string","const":"subscription.canceled"},"createdAt":{"type":"string","description":"ISO 8601 timestamp of when the event was generated."},"isSandbox":{"type":"boolean","description":"true if this event was generated by sandbox (test-key) activity — route accordingly if you share one endpoint for both."},"data":{"type":"object","properties":{"subscriptionId":{"type":"string"},"offerId":{"type":["string","null"]},"priceId":{"type":["string","null"]},"reason":{"description":"Present only for auto-cancellation by the billing cron (the pricing option's maxCycles or maxFailedCycles reached). Absent when the merchant canceled via POST /subscriptions/{subscriptionId}/cancel.","type":"string","enum":["max_cycles_reached","max_failed_cycles_reached"]}},"required":["subscriptionId","offerId","priceId"],"additionalProperties":false}},"required":["id","event","createdAt","isSandbox","data"],"additionalProperties":false,"description":"Fired when a subscription is canceled — either by DELETE /subscriptions/{subscriptionId}, or automatically by the billing cron when maxCycles or maxFailedCycles is reached."},"PaymentSucceededWebhook":{"type":"object","properties":{"id":{"type":"string","description":"Server-generated event id, unique per delivery attempt's underlying event.","example":"evt_01j8x2q3r4s5t6u7v8w9x0y1z2"},"event":{"type":"string","const":"payment.succeeded"},"createdAt":{"type":"string","description":"ISO 8601 timestamp of when the event was generated."},"isSandbox":{"type":"boolean","description":"true if this event was generated by sandbox (test-key) activity — route accordingly if you share one endpoint for both."},"data":{"anyOf":[{"type":"object","properties":{"subscriptionId":{"type":"string"},"invoiceId":{"type":"string"}},"required":["subscriptionId","invoiceId"],"additionalProperties":false,"description":"Shape used when this is a recurring subscription's billing-cycle invoice."},{"type":"object","properties":{"paymentId":{"type":"string"}},"required":["paymentId"],"additionalProperties":false,"description":"Shape used when this is a one-off payment (POST /payments, or a wallet_transfer/Solana client-signed settlement)."}]}},"required":["id","event","createdAt","isSandbox","data"],"additionalProperties":false,"description":"Fired when an invoice settles successfully — either a subscription's billing cycle, or a one-off payment. The data shape depends on which."},"PaymentFailedWebhook":{"type":"object","properties":{"id":{"type":"string","description":"Server-generated event id, unique per delivery attempt's underlying event.","example":"evt_01j8x2q3r4s5t6u7v8w9x0y1z2"},"event":{"type":"string","const":"payment.failed"},"createdAt":{"type":"string","description":"ISO 8601 timestamp of when the event was generated."},"isSandbox":{"type":"boolean","description":"true if this event was generated by sandbox (test-key) activity — route accordingly if you share one endpoint for both."},"data":{"anyOf":[{"type":"object","properties":{"subscriptionId":{"type":"string"},"invoiceId":{"type":"string"},"reason":{"type":"string","example":"insufficient_allowance","description":"Observed values from subscription invoices: insufficient_allowance, insufficient_balance, pull_reverted."}},"required":["subscriptionId","invoiceId","reason"],"additionalProperties":false,"description":"Shape used when this is a recurring subscription's billing-cycle invoice."},{"type":"object","properties":{"paymentId":{"type":"string"},"reason":{"type":"string","example":"insufficient_allowance","description":"Observed values from one-off payments: insufficient_allowance, insufficient_balance, pull_reverted, forward_reverted, platform_transfer_reverted, distribute_reverted."}},"required":["paymentId","reason"],"additionalProperties":false,"description":"Shape used when this is a one-off payment (POST /payments)."}]}},"required":["id","event","createdAt","isSandbox","data"],"additionalProperties":false,"description":"Fired when an invoice fails — either a subscription's billing cycle, or a one-off payment. The data shape depends on which. Note: one-off payments that fail with unknown_token or missing_from_wallet do not fire this webhook at all."},"PaymentCanceledWebhook":{"type":"object","properties":{"id":{"type":"string","description":"Server-generated event id, unique per delivery attempt's underlying event.","example":"evt_01j8x2q3r4s5t6u7v8w9x0y1z2"},"event":{"type":"string","const":"payment.canceled"},"createdAt":{"type":"string","description":"ISO 8601 timestamp of when the event was generated."},"isSandbox":{"type":"boolean","description":"true if this event was generated by sandbox (test-key) activity — route accordingly if you share one endpoint for both."},"data":{"type":"object","properties":{"paymentId":{"type":"string"}},"required":["paymentId"],"additionalProperties":false}},"required":["id","event","createdAt","isSandbox","data"],"additionalProperties":false,"description":"Fired after POST /payments/{paymentId}/cancel cancels a one-off payment. Only reachable while the payment is still \"pending\" — once a pull is broadcast it can no longer be canceled."},"WalletUpdatedWebhook":{"type":"object","properties":{"id":{"type":"string","description":"Server-generated event id, unique per delivery attempt's underlying event.","example":"evt_01j8x2q3r4s5t6u7v8w9x0y1z2"},"event":{"type":"string","const":"wallet.updated"},"createdAt":{"type":"string","description":"ISO 8601 timestamp of when the event was generated."},"isSandbox":{"type":"boolean","description":"true if this event was generated by sandbox (test-key) activity — route accordingly if you share one endpoint for both."},"data":{"type":"object","properties":{"tokenKey":{"type":"string","description":"Which chain+token catalog key this settlement wallet is for.","examples":["usdc-base"]},"previousWallet":{"description":"The address that was registered before this change, or null if none was set yet.","type":["string","null"]},"newWallet":{"description":"The address now registered for this token, or null if it was REMOVED rather than changed to another address. A null here does not mean payouts stop — they fall back to any other wallet the merchant has registered, or to the destination named on each offer's own pricing option.","type":["string","null"]},"source":{"type":"string","enum":["api","dashboard"],"description":"Which surface made the change — the public API (a merchant's own integration, using their API key) or the dashboard (a logged-in session)."}},"required":["tokenKey","previousWallet","newWallet","source"],"additionalProperties":false}},"required":["id","event","createdAt","isSandbox","data"],"additionalProperties":false,"description":"Fired whenever a merchant's settlement wallet for a token is registered, changed, or removed — via POST/DELETE /v1/wallets or the dashboard's Settings > Payments. This redirects (or falls back) where future payouts for that token settle, so it is sent as a security signal: if you did not make this change, treat your API key and dashboard session as compromised and rotate them immediately."}},"securitySchemes":{"apiKey":{"type":"apiKey","in":"header","name":"X-API-Key","description":"Your merchant API key, shown once at creation in the dashboard (Settings → API key)."}}}}