Agent-readable docs index: /llms.txt. Full docs in one file: /llms-full.txt. Download /docs.zip to grep all markdown files locally.

Cancel, resume and close a subscription

The React widget covers this with ManageButton. Without it, each action is two calls from your server with the wallet in between. The API key alone changes nothing: only the wallet that pays a subscription can sign.
ToBuildConfirmIt becomes
Stop the renewals of a running subscriptioncancel(id)confirmCancel(id, { signature })cancelled
Take a cancellation back before its endresume(id)confirmResume(id, { signature })active
Close one that is over, and return its rentclose(id)confirmClose(id, { signature })ended, end_reason: "closed"
Close takes a subscription that was cancelled and whose end has passed, one that ended with its plan, and at once one stopped with stop_reason authority_closed. A subscription stopped for any other reason (out_of_retries, retry_deadline_passed) is cancelled first, then closed: close alone is refused with subscription_not_cancelled. closable and closable_from, on the subscription and on the access answer, say whether it can close now, and from when. One that ended with its plan keeps its end_reason, plan_ended for instance, once closed.

The three steps

They are the same for the three actions. Here with cancel:
  1. Build, on your server
    const { transaction } = await mesub.subscriptions.cancel(subscriptionId);
    Find the customer's subscription with subscriptions.list. Send transaction to your page. Nothing is cancelled yet.
  2. Sign and send, in the browser
    Unlike subscribing, the wallet sends this one itself and pays its network fee. Mesub signs nothing of it.
    import { VersionedTransaction } from '@solana/web3.js'; // wallet and connection: for instance useWallet() and useConnection() of // @solana/wallet-adapter-react, with the paying wallet connected. const bytes = Uint8Array.from(atob(transaction), (c) => c.charCodeAt(0)); const signature = await wallet.sendTransaction( VersionedTransaction.deserialize(bytes), connection, );
    Send signature back to your server.
  3. Confirm, on your server
    const { subscription, reason, reason_code } = await mesub.subscriptions.confirmCancel(subscriptionId, { signature }); if (reason_code === undefined) { // cancelled: subscription.access_until says when access ends } else { // nothing changed: build a new one to try again }
    Branch on reason_code (transaction_expired, cancellation_not_landed, ...), never on reason, a sentence for a person that may be reworded. Keep a default branch: Mesub adds codes.
    Mesub waits for the chain, up to 90 seconds, and answers the subscription as it now stands.
A confirm that timed out changed nothing by itself: send it again with the same signature. A transaction built and never sent changes nothing either.

What cancelling does

A cancellation is not an immediate stop. A subscription that is paid up keeps its access until the end of the period it paid for, and can be resumed until then. Once that date has passed it reads ended, and closing it returns the rent the wallet paid when subscribing.
A subscription cancelled while unpaid or stopped has no access, and its missed period is never collected.
On a plan with an end date, access never runs past the plan's end, cancelled or not. The last period is charged in full even when the plan ends inside it, and the terms the customer signed say so.
Lifecycle has the detail, and the plan's end date too.

Refusals

Each is thrown as a MesubError. apiCode says which:
apiCodeWhen
subscription_not_foundNo subscription of your project under that id
subscription_not_activecancel: it is not active, unpaid or stopped
subscription_cancelledcancel: it was cancelled already
subscription_not_cancelledresume or close: nothing cancelled it. Cancel a stopped one first
subscription_endedresume: the paid period is over. Subscribe again
plan_removedresume: its plan is gone
plan_replacedresume: another plan stands at its address
close_too_earlyclose: its end has not passed yet
subscription_not_on_chainIt is closed already
nothing_to_confirmA confirm before any transaction was built
These six calls are limited to 60 a minute per API key. Each has its page in the reference, from cancel to confirm a close.

Pay a late payment now

A customer who topped up their wallet does not have to wait for the next retry. retry charges the missed period now, with nothing to sign: the wallet approved the plan when it subscribed.
const subscription = await mesub.subscriptions.retry(subscriptionId); // still unpaid: the charge runs in the background
The answer is the subscription, still unpaid. The outcome comes as the subscription.renewed or subscription.payment_failed webhook, and on the subscription. The React widget shows it as Pay now, through POST /subscriptions/:id/retry of the routes, on a late subscription that can pay (payable), and useSubscriptions gives payNow(id) for a list of your own. POST /v1/subscriptions/:id/retry is the call without the SDK.
It is refused before anything is queued when it cannot succeed, as the chain reads now, so no retry is spent for nothing:
apiCodeWhen
insufficient_balanceThe wallet holds less than the period costs. Top up first
retry_cannot_succeedThe wallet no longer approves it, its token account is frozen, or its authority is closed
retry_too_soonThe wait since the last try is not over. retryAfter says when
subscription_not_lateIt is not unpaid
subscription_pausedIts seat is parked
retries_spentOn Free, the three retries of the period are spent
retry_deadline_passedOn Free, retry_deadline has passed
plan_endedIts plan has ended
subscription_changedTwo retries met, or a charge moved it on. Retryable: read it back first
pull_runningOn Free, a charge runs now. Retryable
A 503 changed nothing: unavailable when the charge could not be queued, or network_unavailable. retryAfter says when to ask again.
How often:
TierRetries by hand
FreeThree per missed period, whoever makes them, at least 10 minutes apart, none in the last 2 minutes of the missed period
Dev, BusinessBeside the plan's own retries, uncounted. The first at once, then 20 minutes, 40, 80, doubling
The same retry is in the dashboard. retry is sent once, never retried by the SDK, and limited to 60 a minute per API key.

List its payments

const { data, has_more, paid } = await mesub.subscriptions.attempts(id); for await (const attempt of mesub.subscriptions.allAttempts(id)) { // every page }
attempts answers the charges of that subscription, newest first, 20 a page. paid is counted by Mesub over every paid charge since the subscription began, not over the page.
See a full answer
{ "data": [ { "id": "cmg9x4k2c0007", "attempted_at": "2026-02-01T00:04:07.512Z", "outcome": "paid", "reason": null, "amount": "9990000", "amount_received": "9990000", "amount_usd": "9.99", "signature": "5wHu1qwD4kLDLoREEPBpKvGvhbxWFxFFfr5nQq...", "retry": true, "retry_number": 1, "retries_allowed": 3, "period_start": "2026-01-31T00:00:00.000Z" }, { "id": "cmg9x1b7a0003", "attempted_at": "2026-01-31T00:02:03.000Z", "outcome": "rejected", "reason": "insufficient_balance", "amount": "9990000", "amount_received": null, "amount_usd": "9.99", "signature": null, "retry": false, "retry_number": null, "retries_allowed": null, "period_start": "2026-01-31T00:00:00.000Z" }, { "id": "cmg1x0abd0002", "attempted_at": "2026-01-01T00:00:05.000Z", "outcome": "paid", "reason": null, "amount": "9990000", "amount_received": "9990000", "amount_usd": "9.99", "signature": "3kPzT9yLmR4vP7wJ8sN2aHcVbE5uYtKfDgQxWz1o...", "retry": false, "retry_number": null, "retries_allowed": null, "period_start": "2026-01-01T00:00:00.000Z" } ], "has_more": false, "paid": { "count": 2, "amount": "19980000" } }

Read an attempt

An attempt is one try at charging one period. Mesub keeps every one, whatever came of it: the charge that paid, the one that failed, and the one it never sent.
outcome says how it ended:
outcomeWhat happenedSent to the chainUses a retry
paidThe transfer landed: the period is paidYesNo
skippedMesub read the chain first and found nothing it could chargeNoNo
rejectedThe charge failed on the customer's sideNo, or refused by the programYes
blockedThe charge could not be made, and the customer has no part in itNo, or not known yetNo
Only a paid attempt carries a signature, since only it moved money. A rejected one moved nothing: Mesub reads the wallet's balance before it sends, and a transfer the program refuses changes nothing. A blocked one mostly stopped before sending, and Mesub comes back to it ten minutes later. A block of your plan's own setup that has outlived the customer's access is tried less often: each wait as long as the block has lasted, up to a day, or a quarter of the period on a shorter plan. When a transfer did go out and Mesub could not see it land, it asks the chain before charging again, and records a paid attempt if it landed.
Only rejected counts against the customer. The one at the due date turns the subscription unpaid. Each retry rejected after it is one retry spent, and when the last is spent the subscription turns stopped. A blocked or skipped attempt never uses a retry, however many there are. Neither does a retry fired by hand on Dev or Business, from the dashboard or with retry: it is recorded like any other, and the plan's retries stay as they were.

Reasons

reason says why an attempt did not pay. It is null on a paid one, and a short stable string on every other.
What the customer did, or their wallet:
reasonoutcomeWhat it says
insufficient_balancerejectedThe wallet holds less than the period owes, or has no token account
token_account_reassignedrejectedThe wallet's token account now belongs to someone else
token_account_frozenrejectedThe wallet's token account is frozen
authority_closedrejectedThe wallet is short, and its authorisation is closed for good
program_1rejectedThe wallet holds less than the period owes, or approved less
program_4rejectedThe wallet no longer lets Mesub charge it: the approval was revoked
program_103, 136rejectedThe wallet closed its authorisation, or made a new one since
program_508rejectedCancelled on chain, and its expiry has passed
cancelledskippedCancelled on chain, and the period already paid is over
closed_elsewhereskippedThe subscription no longer exists on chain. It ends, closed_elsewhere
program_103 and program_136 count against the customer only once Mesub reads the authorisation and finds it closed. Then the subscription stops at once, stop_reason authority_closed, whatever the tier and the retries left. Until a read agrees, the attempt is blocked, and tried again.
late_reason reads four of these: insufficient_balance, program_4 (approval_revoked), and program_103, program_136 or authority_closed (authority_closed): see late payments.
What you did to the plan:
reasonoutcomeWhat it says
plan_removedskippedThe plan was deleted. The subscription ends, plan_removed
plan_replacedskippedAnother plan stands at its address. It ends, plan_replaced
plan_endedskippedThe plan's end date has passed. It ends, plan_ended
receiver_not_allowedblockedThe plan's receiver is not among the destinations it allows
receiver_account_missingblockedNeither the receiver nor your wallet has an account for that token
receiver_account_frozenblockedThe token account the plan pays is frozen
receiver_account_needs_memoblockedThat token account requires a memo, which Mesub's charges lack
receiver_account_confidential_onlyblockedThat token account takes confidential transfers only
mint_pausedblockedThe token's issuer paused it. Mesub charges again once it is lifted
mint_transfer_hookblockedThe token's issuer added a transfer hook, which Mesub does not support yet
puller_not_allowedblockedMesub's puller was taken off the plan
program_130, 501, 506, 516, 519blockedOne of the changes above, made while the charge was on its way
mint_paused and mint_transfer_hook are the token issuer's doing, not yours: only the issuer lifts them. Mesub mails you when one holds your charges.
What Mesub or the network did:
reasonoutcomeWhat it says
period_already_paidskippedThe chain holds the period as paid. Not a failure: amount is 0
period_missedskippedA whole period went by before Mesub charged it. It is never collected
terms_missingblockedNo terms were signed through Mesub. Never charged, and not tried again
puller_missingblockedMesub's own record of the plan is incomplete
mint_goneblockedThe token could not be read
gone_unconfirmedblockedThe plan or the subscription read as gone once. Mesub reads it again
send_unconfirmedblockedA transfer is being sent, or its send was cut short
landing_unknownblockedAn earlier transfer may have landed. Mesub waits for the chain to say
program_400blockedThe program says the period is charged already: its clock is behind
solana_<code>blockedAny other error of the chain, with its number
transportblockedThe network did not answer, or not in time
program_<code> comes under two outcomes: rejected for 1, 4, 103, 136 and 508, the codes that are the customer's doing, and blocked for every other code, those these tables name and any number nobody placed. This list can grow, and <code> is any number the chain answers, so keep a default branch on both reason and outcome.

The other fields

FieldWhat it saysNull when
attempted_atWhen Mesub recorded the attemptNever
amountWhat was asked for, in the token's smallest unit, as a stringNever
amount_receivedWhat you received: amount less a Token-2022 transfer feeNot paid, or recorded before Mesub kept it
amount_usdamount in US dollars, at the token's price when it was chargedNot priced yet, or no price was found
signatureThe transaction that paidThe outcome is not paid
retryTrue when a charge for this period had already failedNever
retry_numberWhich retry it was. 1 is the first after the missed chargeIt is not a retry
retries_allowedHow many retries the period had when the attempt ranIt is not a retry
period_startThe start of the period it was forMesub stopped before reading the period
paid, on the answercount and amount of every paid attempt of the subscriptionNever. 0 and "0" when none
retry_number and retries_allowed are kept as the attempt ran: changing the plan's retry policy later does not rewrite them. Both are also null, with retry true, on a retry fired by hand on Dev or Business, which is none of the plan's, and on the few attempts written while the subscription was changing under them. On Free every retry is fired by hand: those are the plan's, and carry both.
period_start is null on the reasons Mesub meets before it reads the period: cancelled, closed_elsewhere, the three plan_ ones, terms_missing, gone_unconfirmed and landing_unknown, and on a transport or solana_<code> that failed on the very first read. On those, amount is the plan's price, where everywhere else it is what the period still owed. On a paid attempt it is what moved.
amount_usd is a decimal string, "9.99". Unlike the plan's, it is not an estimate that moves: Mesub prices each attempt once, paid or not, at the instant of the charge, and never rewrites it. That takes a moment after the attempt is recorded, so read null as "not priced yet" and show the token amount alone until it comes. It stays null for a token no price was found for. A receipt shows amount in the token first: that is what moved. How to name the token is in Show the plan and its token.
The first attempt of a subscription is the payment the customer signed when subscribing, and its signature is that transaction.