Sandbox live

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_code in 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: true and may remint setup_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:

  1. Salesforce Account
  2. provider_code
  3. Provider Integration Config (Active, API enabled, Environment, Account, branding, optional success/cancel URLs)
  4. Server-side API key
  5. ALLOWED_ORIGINS for the provider enrolment origin
  6. 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.