Already On Plan
already_on_plan400
The account is already subscribed to this plan. No change was made.
What this means
A request tried to subscribe an account to a plan it's already on. We refuse the request rather than processing a duplicate charge or modifying the existing subscription's billing cycle. The account's current plan, limits, and renewal date are unchanged. No money moved.
When you'll see this
- A user clicked "Subscribe to Builder" while already on Builder (often a UI race condition where stale subscription state was displayed).
- A retry-on-error logic re-submitted a checkout request that had actually succeeded the first time.
- An automated migration script tried to "ensure" a plan that was already active.
- A user navigated back to checkout after completing payment in another tab.
Learn more about how this works
Asterwise enforces one active plan per account. Upgrading to a higher tier or downgrading to a lower one is a different flow than fresh subscription — those go through /v1/billing/upgrade, not /v1/billing/subscribe. Hitting subscribe on the current plan does nothing useful, so we surface a clear signal rather than silently no-op'ing.
In practice: this error usually points to stale UI state. The user thinks they're on plan A and trying to upgrade to plan B, but they were already moved to plan B in a previous session. Refreshing the dashboard usually clears the confusion.
Example response
{
"success": false,
"error": "already_on_plan",
"message": "You are already on this plan.",
"details": [],
"retry_after": null,
"doc_url": "https://docs.asterwise.com/reference/errors/already_on_plan",
"request_id": "req_01HXYZABCDEFGH",
"timestamp": "2026-05-25T12:34:56Z"
}
- Refresh asterwise.com/dashboard — your current plan will be displayed accurately.
- If you wanted to upgrade or downgrade, use the dedicated upgrade/downgrade flow, not "Subscribe".
- If the dashboard shows a different plan than you're being told you're already on, email [email protected] with the
request_id— that's a state-sync issue we should look at.
This isn't a failure — it's confirmation that no change was needed. Treat it as success in your subscription-management code.
Python:
Production handler
- Python
- TypeScript
import httpx
def ensure_subscription(plan_id, base_url, headers):
response = httpx.post(
f"{base_url}/v1/billing/subscribe",
headers=headers,
json={"plan_id": plan_id},
timeout=15,
)
if response.status_code == 400:
body = response.json()
if body.get("error") == "already_on_plan":
return {"ok": True, "status": "no_change_needed"}
response.raise_for_status()
return {"ok": True, "status": "subscribed", "data": response.json()}
async function ensureSubscription(
planId: string,
baseUrl: string,
headers: HeadersInit,
) {
const response = await fetch(`${baseUrl}/v1/billing/subscribe`, {
method: "POST",
headers,
body: JSON.stringify({ plan_id: planId }),
});
if (response.status === 400) {
const body = await response.json();
if (body.error === "already_on_plan") {
return { ok: true, status: "no_change_needed" };
}
}
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return { ok: true, status: "subscribed", data: await response.json() };
}
Avoid this error by
- Check the current subscription state before showing a "Subscribe" button. If the user is already on the target plan, show "Current plan" instead.
- Disable the Subscribe button immediately after the first click to prevent double-submission races.
- Use the dedicated upgrade endpoint when changing tiers, not the subscribe endpoint.
- Don't use idempotency keys to "force" re-subscription — they don't work that way for this flow.