hasAccess answers true or false:1234567import { Mesub } from '@mesub/node'; const mesub = new Mesub(); // reads MESUB_API_KEY if (!(await mesub.hasAccess({ external_id: user.id }, 'pro'))) { // not subscribed, or no longer }
1234567891011121314import { requirePlan } from '@mesub/node/express'; app.get( '/api/analytics', yourLogin, requirePlan('pro', { customer: (req) => req.user ? { external_id: req.user.id } : null, }), (req, res) => { const { customer, wallet, answer } = res.locals.mesub; res.json(buildAnalytics(customer)); }, );
customer is required. It must come from a session you verified, never from
the request itself: a guard that reads req.query.wallet lets in anyone who
types a subscriber's address.requirePlan(['pro', 'team'], ...): the first plan
that grants access lets the request through. Three plans at most.12curl "https://api.mesub.io/v1/access?external_id=user_42&plan=pro" \ -H "Authorization: Bearer $MESUB_API_KEY"
GET /v1/access.| By | Use it when | Good to know |
external_id | Your app has its own login | Follows the person whichever wallet pays |
wallet | The connected wallet is the customer | A string alone works too |
email | A support lookup | The address they typed when subscribing, never verified by Mesub |
external_id is the id you passed when the subscription was created. The
widget routes pass it for you, from what your customer function returns.hasAccess keeps only access. To show a customer where they stand, access
returns the whole answer:1const answer = await mesub.access({ external_id: user.id }, 'pro');
12345678910111213141516171819202122232425{ "wallet": "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin", "plan": "pro", "subscription_id": "cmg1x0abc0001", "access": true, "status": "active", "paused": false, "end_reason": null, "late_reason": null, "stop_reason": null, "payment_status": "paid", "blocked_since": null, "blocked_reason": null, "subscribed_since": "2026-01-01T00:00:05.000Z", "first_subscribed_at": "2026-01-01T00:00:05.000Z", "current_period_end": "2026-01-31T00:00:00.000Z", "cancelled_at": null, "access_until": "2026-01-31T00:00:00.000Z", "closable": false, "closable_from": null, "next_charge_at": "2026-01-31T00:00:00.000Z", "next_retry_at": null, "retry_deadline": null, "revalidate_after": 300 }
| Field | What it says |
access | The only field a guard needs. False from the plan's end date on |
status | Where the subscription stands: none, pending, active, unpaid, cancelled, stopped, ended, failed, superseded |
payment_status | paid, late or none |
subscription_id | The subscription the answer is about, to cancel, resume, close or retry it without listing first. Null with status none |
late_reason | Why an unpaid one is late, or why a stopped one stopped paying: see late payments. Null otherwise |
stop_reason | Why a stopped one stopped: see late payments. Null otherwise |
blocked_since | Since when its charges are held for a reason that is not the customer's. Null otherwise |
blocked_reason | Why they are held: see blocked charges. Null otherwise |
access_until | When access ends unless a charge renews it, never later than the plan's end date. Null while access is false |
next_charge_at | The next charge. Null when a retry is pending instead, or when the plan ends before it |
next_retry_at | The next retry of a missed charge. Null when the plan ends before it |
closable | Whether the customer can close it now and get its deposit back, as Mesub last read the chain |
closable_from | When it can be closed, if not now. Null when closable is true, or when no date is known yet |
wallet | The wallet that pays. Null when the customer has nothing on that plan |
revalidate_after | Seconds this answer stays true. The SDK caches it for that long |
access to decide, never status. On a plan with an end date, nobody
has access past that date, and for a few minutes after it status can still
read active or unpaid with access: false.123456789{ "access": true, "status": "active", "payment_status": "paid", "blocked_since": "2026-01-31T00:02:00.000Z", "blocked_reason": "puller_not_allowed", "current_period_end": "2026-01-31T00:00:00.000Z", "access_until": "2026-02-03T00:06:00.000Z" }
{ attempts: true }, the answer also carries the last charges.
Read an attempt explains their outcome and
reason.accessList answers every plan the customer has anything on:1const { plans } = await mesub.accessList({ external_id: user.id });
| Status | When | Body |
| 401 | customer returned nothing: nobody is signed in | { access: false, reason: 'unauthenticated' } |
| 402 | Mesub said this customer has no access | { access: false, reason: 'no_access', status } |
| 503 | Mesub could not answer about a customer it never saw | { access: false, reason: 'unavailable' } |
onDenied.GET /v1/access answers 200
with access: false. An error means the question itself was wrong, or could
not be answered.| Status | code in the body | When |
| 400 | invalid_request | Not exactly one of wallet, external_id, email, or one malformed |
| 400 | unsupported_version | Mesub-Version names a version Mesub does not serve |
| 401 | missing_api_key | No API key was sent |
| 401 | invalid_api_key | An API key Mesub never issued, or one rotated since |
| 404 | plan_not_found | No plan of yours under that slug |
| 429 | rate_limited | More than 1,000 access calls a minute on one API key |
| 5xx | internal_error (500), unavailable (503) | Mesub failed, or cannot answer right now |
new Mesub() throws a TypeError
when the server starts.request_id is also in the
Mesub-Request-Id header: quote it to Mesub support.12345678{ "statusCode": 404, "message": "No plan of yours is named pro-max.", "error": "Not Found", "code": "plan_not_found", "retryable": false, "request_id": "req_4f8KzQ2nVb7XcR1mT9wLp3sD" }
MesubError. Its code is the SDK's
own, one per kind of failure, and apiCode is the body's code above:Body code | error.code |
invalid_request, unsupported_version | invalid_request |
missing_api_key, invalid_api_key | unauthorized |
plan_not_found | plan_not_found |
rate_limited | rate_limited |
internal_error, unavailable | unavailable, also a timeout or a network error |
code:12345678910import { MesubError } from '@mesub/node'; try { await mesub.access({ external_id: user.id }, 'pro'); } catch (error) { if (error instanceof MesubError && error.code === 'plan_not_found') { // the slug is wrong: fix the integration } throw error; }
MesubError also carries status, retryable, and retryAfter, the time
Mesub asked to wait, in milliseconds. hasAccess and the guards turn only an
outage into an answer, as described below. A bad key or an unknown plan is
always thrown.revalidate_after. If Mesub then fails,
is too slow, or answers that the rate limit is reached:hasAccess and the guards serve the last answer they knew for that
customer and plan, for up to 24 hours. A paying customer is not locked out
by an outage.hasAccess answers false and a guard
answers 503 with the Retry-After Mesub sent, or 30 seconds when it sent
none.access throws instead, since it is meant for a screen.guardTimeout).