Complete Onboarding Walkthrough
This page is the whole process, end to end, in order: from getting your API key, to learning what your organization and courses look like through the API, to onboarding a student, to what the student receives and how long their access lasts. Every example below is a real response from the platform.
What you get: when someone buys (or you decide to enrol them), one API call creates their account, adds them to your organization, gives them the course(s) you choose for the duration you choose, and emails them a link to set their password. It is safe to repeat, so a retried webhook never creates duplicates.
Base URL: https://<your-platform-host>/api/v1/
Replace <your-platform-host> with your platform's domain and <your-key> with your API key.
| Step | What you do | Endpoint |
|---|---|---|
| 1 | Get an API key | Organization dashboard |
| 2 | Check the key works | GET /ping/ |
| 3 | Look at your organization's defaults | GET /organization/ |
| 4 | See your course types, default courses and courses | GET /courses/ |
| 5 | Onboard a student | POST /provision/student/ |
| 6 | Check a student's access | GET /students/?email= |
| 7 | Renew, extend, add a course, resend the email | POST /provision/student/ again |
| 8 | Automate it (Pabbly, Zapier, Make) | see Automation Setup |
Before you start
- You need an admin (or super‑admin) login for your organization.
- Your organization must have language tests enabled. If it isn't, every onboarding call returns
403. Ask your platform administrator to enable it.
Step 1 — Get your API key
- Sign in and open your Organization dashboard.
- Click More Actions → API Keys & Integrations (super‑admins: Actions → API Keys &
Integrations). The page lives at
https://<your-platform-host>/qbank/organization/<your-org-slug>/api-keys/. - Enter a name such as
Pabbly – productionand click Create key. - Copy the key now. It is shown once and never again, because only a hash of it is stored. If you lose it, revoke it on the same page and create a new one.
The key is your organization. You never send an organization id, and a key can only ever read or change your own organization's students. Treat it like a password and keep one key per tool, so you can revoke one without breaking the others.
Send it on every request as a header:
X-API-Key: <your-key>
(Authorization: Bearer <your-key> works too.)
Step 2 — Check the key works
curl -s https://<your-platform-host>/api/v1/ping/ -H "X-API-Key: <your-key>"
{ "status": "ok", "organization": "Acme Prep", "organization_slug": "acme-prep" }
Your organization's name coming back means the key is valid. A 401 means the key is missing, mistyped
or revoked.
Step 3 — Look at your organization
curl -s https://<your-platform-host>/api/v1/organization/ -H "X-API-Key: <your-key>"
{
"status": "ok",
"organization": { "name": "Acme Prep", "slug": "acme-prep", "language_tests_enabled": true },
"api_key": { "name": "Pabbly – production", "prefix": "2s19bl1d", "created_at": "2026-09-17T14:28:28+00:00" },
"provisioning_defaults": {
"default_access_months": 3,
"grace_period_days": 30,
"starter_tests_per_course": 3,
"visible_tests_limit": 20,
"send_welcome_email_default": true,
"set_password_link_valid_days": 3
},
"counts": { "courses": 1, "students_with_access": 0 },
"as_of": "2026-09-17"
}
These are the rules every onboarding call follows unless you override them in the call:
| Default | Meaning |
|---|---|
default_access_months |
Access length when you don't send tenure_months. |
grace_period_days |
Extra days of access after the end date, before access is removed. |
starter_tests_per_course |
Practice tests placed in the student's dashboard for each new course (0 = off). |
visible_tests_limit |
How many practice tests a new student sees. |
set_password_link_valid_days |
How long the set‑password link in the welcome email works. |
To change a default, ask your platform administrator.
Step 4 — See your course types, default courses and courses
curl -s https://<your-platform-host>/api/v1/courses/ -H "X-API-Key: <your-key>"
{
"status": "ok",
"course_types": [
{ "name": "gmat", "display_name": "GMAT", "default_course": null,
"default_access_months": 3, "starter_tests_available": 0 },
{ "name": "ielts", "display_name": "IELTS", "default_course": null,
"default_access_months": 3, "starter_tests_available": 4 }
],
"courses": [
{ "id": 371, "name": "IELTS Weekend Batch", "course_type": "ielts", "course_type_display": "IELTS",
"is_default": false, "status": "active", "open_for_enrolment": true, "is_perpetual": false,
"start_date": "2026-09-01", "end_date": null,
"default_access_months": 2, "default_access_weeks": 0, "students_with_access": 0 }
]
}
There are two ways to say which course a student gets. Pick one per product, or mix them.
| You send | The student is placed in | Use it when |
|---|---|---|
"courses": ["ielts"] (a course type from course_types[].name) |
Your organization's default course for that type. If you don't have one yet, it is created automatically on the first call ("Quick Enroll - IELTS") and reused from then on. | You sell "IELTS access" and don't need batches. |
"course_ids": [371] (an id from courses[].id) |
That exact course, e.g. a batch or cohort you created in the platform. It must be open_for_enrolment for a new or renewed student. |
You run named batches, or several courses of the same type. |
default_course is null until the first student is placed in it. After that it shows the course id and
name, and the course appears in courses[] with "is_default": true.
Step 5 — Onboard a student (the main call)
curl -s -X POST https://<your-platform-host>/api/v1/provision/student/ \
-H "X-API-Key: <your-key>" \
-H "Content-Type: application/json" \
-d '{
"email": "jane@example.com",
"first_name": "Jane",
"last_name": "Doe",
"courses": ["ielts"],
"tenure_months": 6,
"external_ref": "wc_order_1234",
"source": "pabbly"
}'
The fields:
| Field | Required | What it controls |
|---|---|---|
email |
✅ | The student's identity. The same email always means the same account. |
first_name |
✅ | Shown on the account and in the email. |
last_name |
– | Optional. |
courses |
– | Course type(s) → your default course. "ielts", "ielts, gmat" or ["ielts","gmat"]. |
course_ids |
– | Specific course(s) by id, e.g. [371]. Can be combined with courses. |
tenure_months |
– | How long access lasts, in whole months (1–120), starting today. Omit it to use the default: your organization's default for a course type, or the course's own default for a course_ids course. |
password_mode |
– | How the student gets in: link (default, they set their own password from the email), set (you send the password), or generate (the platform makes one). See Step 5b. |
username |
– | The username for a new account. Left out, it is made from the email address. |
password |
– | The password for a new account, with password_mode of set. |
external_ref |
– | Your reference, such as the order id. Stored for tracing. Required with extend_access. |
send_welcome_email |
– | true by default. false sends nothing, so you can email the student yourself. |
resend_welcome_email |
– | true sends the email again even when nothing changed. |
extend_access |
– | true adds tenure_months to a course the student still has. See Step 7. |
source |
– | A label for your own tracing, e.g. "pabbly". |
Leave out both courses and course_ids to add the person to your organization with no course (for
example a member who will only take your custom tests).
The response (201 because a new account was created; 200 when the email already had an account):
{
"status": "success",
"user_id": 825,
"created_user": true,
"organization": { "name": "Acme Prep", "slug": "acme-prep" },
"membership": { "role": "member", "status": "active", "created": true, "reactivated": false },
"enrollments": [
{
"course_type": "ielts",
"course_type_display": "IELTS",
"course_id": 372,
"course_name": "Quick Enroll - IELTS",
"is_default_course": true,
"enrollment_id": 593,
"status": "active",
"start_date": "2026-09-17",
"end_date": "2027-03-17",
"grace_period_end": "2027-04-16",
"has_access": true,
"action": "created",
"already_enrolled": false,
"starter_tests_assigned": 3
}
],
"credentials": { "username": "jane", "password": null, "must_change_password": false },
"login_url": "https://<your-platform-host>/login/",
"set_password_url": "https://<your-platform-host>/reset/ODI1/df2fhh-3dd0ca02.../",
"set_password_expires_at": "2026-09-20T14:28:29+00:00",
"welcome_email_sent": true,
"email": { "status": "sent", "type": "welcome", "detail": "" }
}
What happened behind that one call:
- Account — Jane's account was created (
created_user: true). An existing account would have been reused, never duplicated. - Organization — she became a member of Acme Prep (
membership). - Course — Acme had no IELTS default course yet, so "Quick Enroll - IELTS" was created and Jane was
placed in it (
action: "created",is_default_course: true). - Duration — access runs from today (
start_date) to six calendar months later (end_date). Thegrace_period_enddate is when access is actually removed. See Access Duration & Renewals. - Ready on day one — full practice access plus 3 starter practice tests in her dashboard.
- Email — the welcome email went out (
email.status: "sent").
Onboarding into a specific course instead looks the same, with course_ids:
curl -s -X POST https://<your-platform-host>/api/v1/provision/student/ \
-H "X-API-Key: <your-key>" -H "Content-Type: application/json" \
-d '{ "email": "ali@example.com", "first_name": "Ali", "course_ids": [371], "external_ref": "wc_order_1300" }'
Shortened response:
{
"status": "success",
"created_user": true,
"enrollments": [
{ "course_id": 371, "course_name": "IELTS Weekend Batch", "is_default_course": false,
"status": "active", "start_date": "2026-09-17", "end_date": "2026-11-17",
"grace_period_end": "2026-12-17", "action": "created", "starter_tests_assigned": 3 }
],
"email": { "status": "sent", "type": "welcome", "detail": "" }
}
No tenure_months was sent, so the course's own default of 2 months applied.
Step 5b — Choose how the student signs in
The API offers the same three options as the Add Student page, through password_mode:
password_mode |
What happens | Student's email | Use it when |
|---|---|---|---|
link (default) |
The account has no password. A one‑time link lets the student choose one. | "Welcome — set your password", with a Set your password button | You want the student to pick their own password. Nothing secret travels through your automation. |
set |
The account is created with the username and password you send. |
"Welcome — your sign‑in details", showing both | Your own system already issues credentials and you want them to match. |
generate |
The platform creates a password and returns it to you. | "Welcome — your sign‑in details", showing both | You want ready credentials to show on a thank‑you page or put in your own message. |
With set or generate the student is asked to change the password the first time they sign in, exactly
as with the Add Student page.
You choose the username and password:
curl -s -X POST https://<your-platform-host>/api/v1/provision/student/ \
-H "X-API-Key: <your-key>" -H "Content-Type: application/json" \
-d '{ "email": "priya@example.com", "first_name": "Priya", "courses": ["ielts"],
"tenure_months": 3, "username": "priya.n", "password": "Monsoon-Study-42!",
"external_ref": "wc_order_1800" }'
Shortened response:
{
"status": "success",
"created_user": true,
"credentials": { "username": "priya.n", "password": null, "must_change_password": true },
"set_password_url": null,
"email": { "status": "sent", "type": "credentials", "detail": "" }
}
A password you chose is not echoed back, since you already have it. Passwords you send are masked in the audit log.
The platform generates the password:
curl -s -X POST https://<your-platform-host>/api/v1/provision/student/ \
-H "X-API-Key: <your-key>" -H "Content-Type: application/json" \
-d '{ "email": "sam@example.com", "first_name": "Sam", "courses": ["ielts"],
"tenure_months": 3, "password_mode": "generate", "external_ref": "wc_order_1700" }'
{
"status": "success",
"created_user": true,
"credentials": { "username": "sam", "password": "uCfkXmxRhF6L", "must_change_password": true },
"email": { "status": "sent", "type": "credentials", "detail": "" }
}
Add "send_welcome_email": false if you would rather put those credentials in your own email. The
response still contains them.
Two rules worth knowing:
- An account that is already in use is never changed. If the email belongs to someone who has signed in,
or to an account created outside this API, its username and password stay exactly as they are, whatever
you send. You get a warning and an explanation instead:
json { "warnings": ["jane@example.com already has an account that has been used, so 'username', 'password' and 'password_mode' were ignored and its sign-in details were not changed. The student can use Forgot password instead."], "credentials": { "username": "priya.n", "password": null, "must_change_password": true, "note": "This account already existed, so its username and password were left unchanged. The student can use Forgot password to set a new one." } }This is what stops a repeated webhook from resetting a student's password and locking them out. - An account you created here and nobody has used yet can still be given credentials. If you onboarded
someone with the default invite and they never signed in, a later call with
setorgeneratedoes apply, because you can already set that account's password with theset_password_urlyou were given. So a test student you created a minute ago will follow whichever mode you send. - Passwords must pass the platform's rules (at least 8 characters, not all digits, not a common
password, not your own email). A weak one is rejected with a
400saying why, and nothing is created.
What the student receives
A new student on the default link mode gets "Welcome to Acme Prep — set your password to get
started". It lists each course with its end date ("IELTS — until 17 Mar 2027"), says how many practice
tests are waiting, and has a Set your password button.
A new student created with credentials gets "Welcome to Acme Prep — your sign‑in details", showing their username and password, the same course list, and a Log in button. It tells them they will be asked to change the password at first sign‑in, and that they can set their own with Forgot password.
- The link works for 3 days (
set_password_expires_at). After that the student uses Forgot password on the login page with the same email. The email tells them this. - After setting a password they sign in at
login_urland see their course.
A student who already signs in gets "Your access at Acme Prep has been updated", listing what they now have, with a Log in button.
There is one email per call at most. Starter practice tests show in the dashboard and as in‑app notifications, without separate emails.
Sending your own email instead: pass "send_welcome_email": false and put set_password_url in your
message. The link is returned for accounts your organization created through the API until the
student signs in for the first time. For an account that existed before (for example someone who signs in
with Google), no link is returned. That protects the account, and the student can always use Forgot
password.
Did the email go out? Read email.status:
email.status |
Meaning |
|---|---|
sent |
Accepted by the mail server. |
queued |
Stored and being retried automatically; it will go out shortly. |
uncertain |
The mail server may have received it but didn't confirm. It is not resent automatically. |
suppressed |
The platform will not mail this address, for example a test or demo domain. |
failed |
It could not be sent. The next call for this student retries a failed welcome automatically, or send "resend_welcome_email": true. |
not_requested |
You sent "send_welcome_email": false. |
not_needed |
Nothing changed and the student already has their email. |
welcome_email_sent is true for sent and queued.
Step 6 — Check a student's access
curl -s "https://<your-platform-host>/api/v1/students/?email=jane@example.com" -H "X-API-Key: <your-key>"
Shortened response (Jane's GMAT course is left out):
{
"status": "ok",
"student": { "user_id": 825, "email": "jane@example.com", "first_name": "Jane", "last_name": "Doe",
"has_set_password": false, "last_login": null },
"membership": { "role": "member", "status": "active" },
"enrollments": [
{ "course_type": "ielts", "course_name": "Quick Enroll - IELTS", "course_id": 372, "enrollment_id": 593,
"is_default_course": true, "status": "active", "start_date": "2026-09-17",
"end_date": "2027-06-17", "grace_period_end": "2027-07-17", "has_access": true }
]
}
has_set_password: false with last_login: null means Jane hasn't opened her welcome email yet. You get
a 404 for anyone who isn't your organization's student.
Step 7 — Everyday changes, all through the same call
Call POST /provision/student/ again with the same email:
| You want to… | Send | Result |
|---|---|---|
| Retry a webhook | The same body again | Nothing changes, nothing is re‑sent: status: "already_provisioned", action: "unchanged". |
| Add a second course | "courses": ["gmat"] |
GMAT is added alongside IELTS (action: "created"). |
| Renew after access ended (or during the grace period) | The course + a new tenure_months |
The same enrolment gets a fresh window from today (action: "reactivated"). |
| Extend while access is still running | The course + tenure_months + "extend_access": true + a new external_ref |
Months are added to the current end date (action: "extended"). The same external_ref never extends twice. |
| Resend the welcome email | "resend_welcome_email": true |
The email goes out again with a fresh link. |
| Give access to someone suspended | Not through the API | Suspensions are left alone (action: "skipped", skipped_reason: "suspended"). Lift it in the platform. |
Full details, with real before and after dates, are on Access Duration & Renewals.
Step 8 — Automate it
Everything above is one HTTP POST, so any automation tool can run it without code. A typical flow:
WooCommerce order paid → Pabbly / Zapier / Make → POST /api/v1/provision/student/ → student onboarded + emailed
Step‑by‑step recipes are on Automation Setup.
Go‑live checklist
Tick these off once before switching your automation on:
GET /ping/returns your organization.GET /organization/shows the duration and grace you expect.GET /courses/lists the course types or course ids your products map to.- One test student is onboarded with your own email address, and the welcome email arrived.
- You set that student's password from the email and saw the course after signing in.
- Running the same call again returned
already_provisionedand sent no second email. - Your automation stops on
4xxand retries only on5xx.
