Enrolment Integration
Enrolment Integration is the NZ product where the education provider owns the enrolment UX and calls the StudentPay Provider Checkout API from its backend.
This is not Hosted Enrolment Checkout. StudentPay does not host your course catalogue or branded enrolment form in this product. Do not embed or copy the hosted app as a stand-in for Integration.
Canonical routes:
POST /v1/provider-checkouts
GET /v1/provider-checkouts/{checkoutId}
POST /v1/provider-checkouts/{checkoutId}/confirm
Do not call /api/demos/bela-beauty/*. Those routes are a sandbox
demonstration UI only.
Who owns what
| Provider owns | StudentPay owns |
|---|---|
| Course / enrolment UI | Bearer authentication and PIC resolution |
| Student data capture | Course catalogue and commercial terms |
Approved course.course_code (authoritative providers) |
Payload validation and plan maths |
Permitted preferences (first_payment_date, allowed frequency) |
Contact, Opportunity, draft DDA |
| Plan selection UX within allowed frequencies | Hosted BECS NZ setup_url |
Redirecting the payer to setup_url |
setup_complete vs Authorised |
| Collecting on-screen declarations | Confirm, Payment Plan Agreement, idempotent retries |
| Return UX after bank setup | Final checkout status |
Catalogue-authoritative providers send a course identifier. StudentPay
resolves price and instalment maths. Legacy providers without course_code
still supply commercial fields. See Payment Plans
and Current contract.
Environments
| Environment | Base URL |
|---|---|
| Sandbox | https://sandbox-api.studentpay.co.nz |
| Production | https://api.studentpay.co.nz |
Keys, Salesforce orgs, and GoCardless accounts are isolated. A sandbox key does not work in Production. See Environments.
Authentication
Call StudentPay from your server.
Authorization: Bearer <YOUR_API_KEY>
Content-Type: application/json
Idempotency-Key: YOUR-ENROLMENT-1001
- The key is issued per provider and must stay server-side
- The key determines provider identity
provider.provider_codein the body must match the key- You cannot select another provider by changing
provider_code
Never put the key in browser JavaScript. See Authentication.
provider_order_id
provider.provider_order_id is your stable enrolment identifier.
- You generate it
- It must be unique within your provider
- Retries for the same enrolment must reuse the same value
- A replay returns the existing checkout with
idempotent_replay: true
A retry with the same order id and the same canonical enrolment intent
returns the existing checkout. A retry with a materially different
canonical intent (course, price, plan, first payment date) returns 409
IDEMPOTENCY_CONFLICT.
Supported contract: reuse the original logical enrolment for the same order id. Do not reuse an order id to mean a different course, price, or plan.
See Idempotency.
1. Create
POST /v1/provider-checkouts
Catalogue-authoritative create (preferred)
For OLI_NZ and BELA_NZ, send the approved course identifier. Do not invent
price or instalment maths.
{
"provider": {
"provider_code": "OLI_NZ",
"provider_order_id": "ORDER-123"
},
"student": {
"first_name": "Alex",
"last_name": "Student",
"email": "alex.student@example.com"
},
"course": {
"course_code": "PSY101"
},
"plan": {
"payment_frequency": "Weekly",
"first_payment_date": "2026-10-01"
}
}
StudentPay resolves PSY101 to $1,834.25, weekly $25.00, and
73 × $25.00 + 1 × $9.25. Create echoes the resolved pricing and plan.
If you also send pricing.course_price, plan.instalment_amount, or
plan.number_of_instalments, they must match those resolved values or create
returns 400 VALIDATION_ERROR.
Unknown, inactive, other-provider, or wrong-environment course_code values
are rejected. Currency remains NZD.
Legacy create (providers without a configured course catalogue)
Providers that are not catalogue-authoritative may omit course.course_code
and continue to supply commercial fields. StudentPay validates the maths.
{
"provider": {
"provider_code": "SANDBOX_DEMO",
"provider_order_id": "DEMO-ENROLMENT-1001"
},
"student": {
"first_name": "Alex",
"last_name": "Student",
"email": "alex.student@example.com"
},
"course": {
"course_name": "Example Certificate"
},
"pricing": {
"course_price": 1834.25,
"amount_to_finance": 1834.25,
"upfront_payment": 0
},
"plan": {
"payment_type": "interest_free_payment_plan",
"payment_frequency": "Weekly",
"number_of_instalments": 74,
"instalment_amount": 25,
"final_instalment_amount": 9.25,
"first_payment_date": "2026-10-01"
}
}
73 × $25.00 + 1 × $9.25 = $1,834.25. The final instalment may differ from the
regular instalment so that principal is conserved exactly.
Create returns a public checkout object. Typical fields:
| Field | Meaning |
|---|---|
checkout.checkout_id |
StudentPay checkout id. Use it on GET and confirm |
checkout.status |
Provider-facing checkout status |
records.opportunity_id |
Enrolment identifier used on confirm |
direct_debit.setup_url |
Payer browser URL for BECS NZ setup |
direct_debit.token |
Setup token; send as checkout.checkout_token on confirm |
direct_debit.setup_complete |
false until the payer finishes GoCardless |
plan |
Echo of validated plan values (including residual when sent) |
Invalid commercial maths returns 400 VALIDATION_ERROR before financial
records are created.
The records.* and agreement.* identifiers are opaque correlation ids on
this API. Do not depend on Salesforce object internals beyond those fields.
2. Direct debit
NZ enrolment uses GoCardless and BECS NZ.
Open direct_debit.setup_url in the payer’s browser. That page is hosted by
StudentPay / GoCardless. You do not send your API key there. You do not create
Billing Requests yourself.
Enrolment creates a mandate flow. Confirm does not collect the first
regular instalment or any initial payment. After confirm, Salesforce originates
the Charge_Schedule rows, including one initial-payment row when
upfront_payment > 0. The existing scheduled-payment processor submits due
schedules through GoCardless.
Poll:
GET /v1/provider-checkouts/{checkoutId}
Confirm only when direct_debit.setup_complete is true. That currently
allows mandate pending_submission, submitted, or active.
Setup complete is not Authorised. You do not wait for
authorisation_status = Authorised or mandate_status = active.
Details: Direct Debit Setup.
3. Confirm
POST /v1/provider-checkouts/{checkoutId}/confirm
{
"provider": {
"provider_code": "SANDBOX_DEMO",
"provider_order_id": "DEMO-ENROLMENT-1001"
},
"checkout": {
"checkout_id": "SANDBOX_DEMO-1720000000000-AB12CD34",
"checkout_token": "<from create>",
"opportunity_id": "006XXXXXXXXXXXX",
"dda_id": "a0XXXXXXXXXXXXX"
},
"payment": {
"payment_method": "studentpay_payment_plan",
"first_payment_date": "2026-10-01",
"deposit_confirmed": true
},
"declarations": {
"payment_plan_accepted": true,
"information_confirmed": true,
"privacy_consent_accepted": true
}
}
All three declarations and payment.deposit_confirmed must be true.
deposit_confirmed is a required acceptance field. Confirm does not collect
the initial payment. Collection, if any, is a later Direct Debit against the
originated Charge_Schedule.
A successful first confirm returns checkout.status: confirmed and
enrolment.status: complete, with Payment Plan Agreement references.
A repeat confirm returns already_confirmed: true and does not create a
second agreement or schedule.
4. Payment Plan Agreement
First confirm generates a Payment Plan Agreement and stores the PDF.
The confirm response includes agreement number / id and pdf_generated.
There is no public download endpoint. Operations retrieve the file through
StudentPay’s back office if needed.
See Agreements.
5. Schedule
After confirm, Salesforce originates the charge schedule. Your integration does not submit collections.
When the plan does not divide evenly, the last instalment is the residual. Example:
| Course price | $1,834.25 |
| Upfront | $0.00 |
| Weekly instalment | $25.00 |
| Schedule | 73 × $25.00 + 1 × $9.25 |
| Total | $1,834.25 |
See Payment Plans.
Idempotency
Retries are safe when you reuse provider_order_id. See Idempotency.
- Create: send
Idempotency-Key, or StudentPay uses{provider_code}:{provider_order_id} - Replay returns
idempotent_replay: trueand may remintsetup_url/ tokens - Confirm replay returns
already_confirmed: true
Errors
| HTTP | Typical code | When |
|---|---|---|
| 401 | MISSING_API_KEY |
No Bearer token |
| 403 | INVALID_API_KEY / PROVIDER_KEY_MISMATCH |
Unknown key, or key does not match provider_code |
| 403 | PIC unusable | Inactive, API disabled, or Environment mismatch |
| 400 | VALIDATION_ERROR |
Missing/invalid fields, unknown course_code, or plan/price does not match the catalogue |
| 404 | PROVIDER_NOT_FOUND / CHECKOUT_NOT_FOUND |
Unknown PIC or checkout |
| 409 | DIRECT_DEBIT_SETUP_INCOMPLETE |
Confirm before setup complete |
| 409 | CONFLICT |
Confirm identifiers do not match the stored checkout |
| 409 | IDEMPOTENCY_CONFLICT |
Same provider_order_id with a different canonical enrolment intent |
| 5xx | INTERNAL_ERROR |
Unexpected failure; response must not include secrets |
Details: Errors.
Onboarding a provider
StudentPay configures:
- Salesforce Account
provider_code- Provider Integration Config (Active, API enabled, Environment, Account, branding, optional success/cancel URLs)
- Server-side API key
ALLOWED_ORIGINSfor the provider enrolment origin- Approved course / product identifiers when the provider is catalogue-authoritative
Then the provider certifies against sandbox /v1 using
Sandbox testing. Production is a separate cutover:
Production onboarding.