billing

POST /v1/trials/{trial_id}/switch

Switch a running free trial to another product, keeping its end date.

All billing endpoints

All developer docs →

Authentication

Send an API key as a bearer token. The endpoint checks billing.view; a key without it is refused with 403, not 404.

This endpoint takes no organisation id. Your key already identifies the organisation it belongs to, and the response is scoped to it.

Try it

Replace anything in angle brackets with your own values, and the key placeholder with a key from your dashboard.

curl -X POST https://api.zinndigital.com/v1/trials/{trial_id}/switch \
  -H "Authorization: Bearer zdk_live_…" \
  -H "Content-Type: application/json" \
  -d '{ "plan_code": <string> }'

Signed in? The API console in your dashboard fills in your real organisation id and your own key, and runs the request against the live API so you can see the actual response. Open this endpoint in the API console

Details

s1014 L58. ⚖️ Owner 2026-10-16: "within the first 3 days a trial can be switched to a different product keeping the remaining days". Allowed once per trial, in its first three days, to a product this account never trialled and has no plan on. The product it leaves counts as trialled (it cannot be trialled again). Refused while the trial has a live site (refusal.trial.switch.liveSite) — remove the site, then switch. The new trial does not carry an auto-convert consent. Each refusal names its reason as refusal.trial.switch.<code>.

Parameters

NameTypeRequiredWhat it is
trial_id (path)stringYes—

Request body

NameTypeRequiredWhat it is
plan_codestringYesA plan of the product to switch the trial to (its line's trial applies).

Response

NameTypeRequiredWhat it is
idstringYes—
org_idstringYes—
statusstring<active, expired, converted, canceled, switched>Yes—
product_linestringYesThe line's CODE, not its display name — the dashboard renders the label from its own catalogue, so prose here would be a second, untranslated source of that word on the screen.
max_sitesintegerYes—
started_atstringYes—
ends_atstringYes—
auto_convert_opt_inbooleanNoWhether the customer agreed at the start that this may become a paid subscription when the period ends. Captured per trial rather than read from the template at expiry, so…
days_remainingintegerNoWhole days until ends_at, rounded UP and floored at zero. Computed on the server on purpose: a client doing (ends_at - Date.now()) measures the device clock, so a laptop a…
upgradeTrialUpgradeOfferNoThe plan this trial invites the customer to buy, priced in their own currency (X-Zinn-Currency, falling back to the org default). ⛔ null when the template names no target or…
switch_deadlinestringNos1014 L58 (owner 2026-10-16). Until when this trial may still be switched to another product (POST /v1/trials/{trial_id}/switch): its first three days. null when it cannot be…
planobjectNos1014 L70. The plan this trial was started for (the one the customer clicked) and the application it installs, so the create-site form opens on that application — a "Moodle Pro"…

Errors this endpoint can return

401 · 403 · 404 · 422