Install our app for a better experience!

Complete Onboarding Walkthrough

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

  1. Sign in and open your Organization dashboard.
  2. 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/.
  3. Enter a name such as Pabbly – production and click Create key.
  4. 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:

  1. Account — Jane's account was created (created_user: true). An existing account would have been reused, never duplicated.
  2. Organization — she became a member of Acme Prep (membership).
  3. 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).
  4. Duration — access runs from today (start_date) to six calendar months later (end_date). The grace_period_end date is when access is actually removed. See Access Duration & Renewals.
  5. Ready on day one — full practice access plus 3 starter practice tests in her dashboard.
  6. 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 set or generate does apply, because you can already set that account's password with the set_password_url you 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 400 saying 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_url and 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:

  1. GET /ping/ returns your organization.
  2. GET /organization/ shows the duration and grace you expect.
  3. GET /courses/ lists the course types or course ids your products map to.
  4. One test student is onboarded with your own email address, and the welcome email arrived.
  5. You set that student's password from the email and saw the course after signing in.
  6. Running the same call again returned already_provisioned and sent no second email.
  7. Your automation stops on 4xx and retries only on 5xx.