Core concepts
How Offers, Subscriptions, Customers, Payments, and Settlement Wallets fit together.
Tidepay exposes six resources. Everything else is an implementation detail.
CUSTOMER
│
│ purchases
↓
OFFER ──────────────→ checkoutUrl
│
pricing[] — one or more options
│
customer picks exactly one
│
┌─────┴──────┐
↓ ↓
one_time recurring
PAYMENT SUBSCRIPTION
│
↓
PAYMENT × NOffers
An Offer is what you sell and on what terms. It carries its own pricing, its own presentation, and its own checkout URL — creating one is a single call, with nothing to set up first.
{
"name": "Pro",
"pricing": [
{
"amount": "30",
"currency": "USDC",
"acceptedTokens": ["usdc-base"],
"billing": { "type": "recurring", "interval": "month" }
}
],
"merchantDestinationWallet": "0x71c7656ec7ab88b098defb751b7401b5f6d8976"
}The response includes a checkoutUrl. That URL is a property of the Offer, not a separate object to create and manage.
Pricing options
An Offer holds one or more pricing options. Each is a complete set of billing terms — amount, currency, accepted chain+token combinations, and billing interval. When an Offer has several, the customer picks one at checkout, and the Subscription records which.
Each option gets a server-assigned id (price_...) so a Subscription can reference it and reporting can group by it. It is referenceable but not manageable: there is no /v1/prices endpoint, and you never create, list, or edit one directly.
- Multiple chains and tokens:
acceptedTokens(e.g.["usdc-base", "usdt-base", "usdc-polygon"]) is the full set an option accepts. The subscriber picks both the chain and the ticker at checkout. - Monthly and annual: two options on one Offer, behind one checkout URL. Add the second with
POST /offers/{offerId}/pricing. - Payout destination:
merchantDestinationWalletis the fallback payout address. A registered settlement wallet for the chosen token takes priority — on every future charge, even for subscriptions created before it was registered. - Payout splitting:
splitEvm/splitSolanadivide each payout among multiple recipients — 0xSplits contracts for EVM (one per accepted EVM chain), direct on-chain fan-out for Solana. Immutable once created.
Pricing is frozen at creation
An option priced in fiat (currency: "BRL") is converted to each accepted token once, when the option is created, and every charge bills that same token amount for its lifetime. The fiat amount/currency stay on the option as the price you advertised; they are not re-quoted per cycle, so a subscriber pays the same number of tokens every time regardless of what the exchange rate does afterwards.
Billing terms are immutable
amount, currency, acceptedTokens and billing cannot be changed after an option is created. This is enforced by the payment rails, not by policy:
- EVM: a subscriber grants an ERC-20 allowance sized against the amount in force when they subscribed. Raising it would exceed what they authorized, and the next cycle would fail.
- Solana: the
@solana/subscriptionsprogram stores the amount and period in an on-chain account that is immutable by construction.
PATCH /offers/{offerId} therefore covers presentation, addressing, distribution limits and lifecycle — never pricing. To sell at a different price, add an option; to retire one, DELETE /offers/{offerId}/pricing/{priceId}; to move an existing subscriber, use migration.
Other option-level settings: trialDays delays the first cycle (authorization still happens immediately), maxCycles caps how many cycles bill before auto-cancelling, and maxFailedCycles auto-cancels after that many consecutive failures — independent of the cron's own 7-day past-due grace window, whichever hits first.
Subscriptions
A Subscription is one customer's recurring relationship with an Offer. It references the Offer, the pricing option chosen, and the customer.
Creating one returns a URL where the subscriber connects a wallet and authorizes the recurring pulls on-chain. How that authorization is stored differs per chain family, and neither gives Tidepay custody of the funds:
- EVM: an ERC-20 allowance granted to Tidepay's operator, sized to cover several cycles.
- Solana: a subscription against the audited
@solana/subscriptionsprogram. The token account's delegate is a program-owned account, not Tidepay's, and the program caps what can be pulled per billing period — so no more than one period's amount can ever be taken, whatever the caller does. The subscriber pays a small, recoverable rent (~0.0036 SOL) for the accounts this creates.
The pricing snapshot
A Subscription records the terms it was created under — the amount, the token amount actually charged, and the billing interval — and bills those, not whatever the Offer says today. Editing an Offer's pricing, adding options, or retiring one cannot reprice an existing subscriber.
Lifecycle
POST /subscriptions/{id}/cancel
POST /subscriptions/{id}/resume
POST /subscriptions/{id}/migrate
POST /subscriptions/{id}/retryCancelling is not deletion. The subscription and its payment history survive, because that history is what reconciliation reads. Tidepay simply stops pulling.
Resuming has two outcomes, reported by reauthorizationRequired. A past_due subscription reactivates immediately — its authorization was never revoked. A canceled one returns an authorizationUrl: cancelling revokes the EVM allowance / closes the Solana delegation account, and only the wallet holder can sign a new one.
Migrating a subscriber
Moving a subscriber to a different pricing option on the same Offer is explicit, and requires their consent:
POST /subscriptions/{id}/migrate
{ "priceId": "price_01j8x2q3r4s5t6u7v8w9x0y1z7" }The response carries 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.
Customers
A Customer is the payer — the wallet(s) that fund a subscription or payment. A customer can hold at most one wallet per chain family (evm, solana) at a time. You can create a Customer before any wallet is known and add one later via PATCH /customers/{customerId}, but a subscription won't start billing until a wallet matching its chosen chain family is present.
Payments
A Payment is a transaction that actually happened. One resource covers all of them:
- a direct one-off pull (
POST /payments), - a one-time Offer checkout,
- and each recurring billing cycle.
source distinguishes them (one_time or subscription), and subscriptionId is set on the recurring ones. There is deliberately no separate invoices resource — one place to look for any transaction.
GET /payments?subscriptionId=sub_01j8x...
GET /payments?source=one_time
GET /payments?offerId=offer_01j8x...A direct POST /payments requires fromWallet to have already granted the operator an on-chain allowance covering amount; Tidepay never holds custody, the operator wallet only transiently forwards funds to toWallet. idempotencyKey is required. Delivery is synchronous — the response reflects the final settled/failed outcome, not an initial pending state.
Currencies
The catalog of chain + token combinations Offers and Payments are built against. Crypto-first: a currency is an asset on a network, not a bare ticker — USDC/Base and USDC/Solana are different entries, with their own decimals and contract addresses.
Settlement Wallets
A registered payout wallet, one per tokenKey you accept. Once registered, it overrides whatever merchantDestinationWallet was sent when an Offer was created for that token — on every future charge, retroactively covering offers and subscriptions created before the wallet existed. Deleting one reverts to each pricing option's own merchantDestinationWallet.