billing
POST /v1/trials/{trial_id}/switch
Switch a running free trial to another product, keeping its end date.
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
| Name | Type | Required | What it is |
|---|---|---|---|
trial_id (path) | string | Yes | — |
Request body
| Name | Type | Required | What it is |
|---|---|---|---|
plan_code | string | Yes | A plan of the product to switch the trial to (its line's trial applies). |
Response
| Name | Type | Required | What it is |
|---|---|---|---|
id | string | Yes | — |
org_id | string | Yes | — |
status | string<active, expired, converted, canceled, switched> | Yes | — |
product_line | string | Yes | The 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_sites | integer | Yes | — |
started_at | string | Yes | — |
ends_at | string | Yes | — |
auto_convert_opt_in | boolean | No | Whether 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_remaining | integer | No | Whole 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… |
upgrade | TrialUpgradeOffer | No | The 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_deadline | string | No | s1014 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… |
plan | object | No | s1014 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