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.| To | Build | Confirm | It becomes |
| Stop the renewals of a running subscription | cancel(id) | confirmCancel(id, { signature }) | cancelled |
| Take a cancellation back before its end | resume(id) | confirmResume(id, { signature }) | active |
| Close one that is over, and return its rent | close(id) | confirmClose(id, { signature }) | ended, end_reason: "closed" |
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.cancel:1const { transaction } = await mesub.subscriptions.cancel(subscriptionId);
subscriptions.list. Send transaction
to your page. Nothing is cancelled yet.123456789import { 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, );
signature back to your server.12345678const { 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 }
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.ended, and closing it returns the
rent the wallet paid when subscribing.unpaid or stopped has no access, and its
missed period is never collected.MesubError. apiCode says which:apiCode | When |
subscription_not_found | No subscription of your project under that id |
subscription_not_active | cancel: it is not active, unpaid or stopped |
subscription_cancelled | cancel: it was cancelled already |
subscription_not_cancelled | resume or close: nothing cancelled it. Cancel a stopped one first |
subscription_ended | resume: the paid period is over. Subscribe again |
plan_removed | resume: its plan is gone |
plan_replaced | resume: another plan stands at its address |
close_too_early | close: its end has not passed yet |
subscription_not_on_chain | It is closed already |
nothing_to_confirm | A confirm before any transaction was built |
retry charges the missed period now, with nothing to sign: the wallet
approved the plan when it subscribed.12const subscription = await mesub.subscriptions.retry(subscriptionId); // still unpaid: the charge runs in the background
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.apiCode | When |
insufficient_balance | The wallet holds less than the period costs. Top up first |
retry_cannot_succeed | The wallet no longer approves it, its token account is frozen, or its authority is closed |
retry_too_soon | The wait since the last try is not over. retryAfter says when |
subscription_not_late | It is not unpaid |
subscription_paused | Its seat is parked |
retries_spent | On Free, the three retries of the period are spent |
retry_deadline_passed | On Free, retry_deadline has passed |
plan_ended | Its plan has ended |
subscription_changed | Two retries met, or a charge moved it on. Retryable: read it back first |
pull_running | On Free, a charge runs now. Retryable |
unavailable when the charge could not be queued, or
network_unavailable. retryAfter says when to ask again.| Tier | Retries by hand |
| Free | Three per missed period, whoever makes them, at least 10 minutes apart, none in the last 2 minutes of the missed period |
| Dev, Business | Beside the plan's own retries, uncounted. The first at once, then 20 minutes, 40, 80, doubling |
retry is sent once, never retried by
the SDK, and limited to 60 a minute per API key.12345const { 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.123456789101112131415161718192021222324252627282930313233343536373839404142434445464748{ "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" } }
outcome says how it ended:outcome | What happened | Sent to the chain | Uses a retry |
paid | The transfer landed: the period is paid | Yes | No |
skipped | Mesub read the chain first and found nothing it could charge | No | No |
rejected | The charge failed on the customer's side | No, or refused by the program | Yes |
blocked | The charge could not be made, and the customer has no part in it | No, or not known yet | No |
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.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.reason says why an attempt did not pay. It is null on a paid one, and a
short stable string on every other.reason | outcome | What it says |
insufficient_balance | rejected | The wallet holds less than the period owes, or has no token account |
token_account_reassigned | rejected | The wallet's token account now belongs to someone else |
token_account_frozen | rejected | The wallet's token account is frozen |
authority_closed | rejected | The wallet is short, and its authorisation is closed for good |
program_1 | rejected | The wallet holds less than the period owes, or approved less |
program_4 | rejected | The wallet no longer lets Mesub charge it: the approval was revoked |
program_103, 136 | rejected | The wallet closed its authorisation, or made a new one since |
program_508 | rejected | Cancelled on chain, and its expiry has passed |
cancelled | skipped | Cancelled on chain, and the period already paid is over |
closed_elsewhere | skipped | The 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.reason | outcome | What it says |
plan_removed | skipped | The plan was deleted. The subscription ends, plan_removed |
plan_replaced | skipped | Another plan stands at its address. It ends, plan_replaced |
plan_ended | skipped | The plan's end date has passed. It ends, plan_ended |
receiver_not_allowed | blocked | The plan's receiver is not among the destinations it allows |
receiver_account_missing | blocked | Neither the receiver nor your wallet has an account for that token |
receiver_account_frozen | blocked | The token account the plan pays is frozen |
receiver_account_needs_memo | blocked | That token account requires a memo, which Mesub's charges lack |
receiver_account_confidential_only | blocked | That token account takes confidential transfers only |
mint_paused | blocked | The token's issuer paused it. Mesub charges again once it is lifted |
mint_transfer_hook | blocked | The token's issuer added a transfer hook, which Mesub does not support yet |
puller_not_allowed | blocked | Mesub's puller was taken off the plan |
program_130, 501, 506, 516, 519 | blocked | One 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.reason | outcome | What it says |
period_already_paid | skipped | The chain holds the period as paid. Not a failure: amount is 0 |
period_missed | skipped | A whole period went by before Mesub charged it. It is never collected |
terms_missing | blocked | No terms were signed through Mesub. Never charged, and not tried again |
puller_missing | blocked | Mesub's own record of the plan is incomplete |
mint_gone | blocked | The token could not be read |
gone_unconfirmed | blocked | The plan or the subscription read as gone once. Mesub reads it again |
send_unconfirmed | blocked | A transfer is being sent, or its send was cut short |
landing_unknown | blocked | An earlier transfer may have landed. Mesub waits for the chain to say |
program_400 | blocked | The program says the period is charged already: its clock is behind |
solana_<code> | blocked | Any other error of the chain, with its number |
transport | blocked | The 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.| Field | What it says | Null when |
attempted_at | When Mesub recorded the attempt | Never |
amount | What was asked for, in the token's smallest unit, as a string | Never |
amount_received | What you received: amount less a Token-2022 transfer fee | Not paid, or recorded before Mesub kept it |
amount_usd | amount in US dollars, at the token's price when it was charged | Not priced yet, or no price was found |
signature | The transaction that paid | The outcome is not paid |
retry | True when a charge for this period had already failed | Never |
retry_number | Which retry it was. 1 is the first after the missed charge | It is not a retry |
retries_allowed | How many retries the period had when the attempt ran | It is not a retry |
period_start | The start of the period it was for | Mesub stopped before reading the period |
paid, on the answer | count and amount of every paid attempt of the subscription | Never. 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.signature is that transaction.