Skip to content
ChoicePaid
Menu

API and webhooks

Version 1. Last updated October 11, 2026. Included with Pro and School.

Connect ChoicePaid to your booking site, CRM, accounting or automation tool. Create students and sessions from wherever they start, read invoices and their status, and get a signed webhook the moment something is paid. Manage keys and webhooks in Account, then Developers.

Authentication

Send your key as a bearer token on every request. Keys start with cp_live_, are shown once when created, and are stored only as a one-way hash. A read-only key can call every GET; changes need a read and write key. Keep keys on your server; never put one in a web page or an app people download.

curl https://www.choicepaid.com/api/v1/me \
  -H "Authorization: Bearer cp_live_your_key"

Errors and limits

Responses are JSON. Errors look like {"error":{"code":"invalid","message":"…"}} with a matching HTTP status: 400 invalid, 401 unauthorized, 403 plan, scope or limit, 404 not_found, 409 transition or checks_failing, 429 rate_limited. Each key can make 120 requests a minute; responses include X-RateLimit-Remaining, and a 429 includes Retry-After. Money is always in cents; dates are YYYY-MM-DD.

Pagination

List endpoints return {"data":[…],"next_cursor":"…"}, newest first. Pass limit (1 to 100, default 50) and send next_cursor back as cursor for the next page. A null next_cursor means you have everything.

Endpoints

MethodPathWhat it does
GET/api/v1/meThe workspace the key belongs to: business name, plan, key scope.
GET/api/v1/studentsStudents, newest first. include_archived=true to include archived.
GET/api/v1/students/:idOne student.
POST/api/v1/studentsAdd a student: name, program (slug), optional parent, grade, family_email. Write key.
GET/api/v1/sessionsSessions. Filters: student_id, from, to (YYYY-MM-DD), status (scheduled, delivered, cancelled).
POST/api/v1/sessionsLog or schedule a session: student_id, date, description, rate_cents; optional quantity, start_time, minutes, status, note, share_note, payer. Write key.
GET/api/v1/invoicesInvoices, receipts and service records. Filters: status, student_id, from, to (issue date).
GET/api/v1/invoices/:idOne invoice with its lines, student name and status history.
PATCH/api/v1/invoices/:idChange status (submitted, approved, paid, rejected, void, with on, reason, rejection_code) or set reference (payment platform order number). Write key.

Status changes follow the same rules as the app: draft to submitted, paid or void; submitted to approved, paid, rejected or void; approved to paid or rejected; rejected back to submitted or void. A draft with a failing program check can't be marked submitted until it's fixed. Voiding releases its sessions so they can be billed again.

Examples

Log a delivered tutoring hour:

curl -X POST https://www.choicepaid.com/api/v1/sessions \
  -H "Authorization: Bearer cp_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{"student_id":"5b0c…","date":"2026-10-09","start_time":"15:30",
       "minutes":60,"description":"Math tutoring, 1:1","rate_cents":6500}'

Mark an invoice paid:

curl -X PATCH https://www.choicepaid.com/api/v1/invoices/9e1f… \
  -H "Authorization: Bearer cp_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{"status":"paid","on":"2026-10-11"}'

Webhooks

Add an https endpoint and pick events. Each event is a POST with a JSON body like this, plus the headers ChoicePaid-Event, ChoicePaid-Delivery (unique per delivery; use it to ignore duplicates) and ChoicePaid-Signature.

{
  "id": "d4c1…",
  "type": "invoice.paid",
  "created": 1791763200,
  "data": {
    "id": "9e1f…", "number": "INV-0042", "student_id": "5b0c…",
    "program": "arizona-empowerment-scholarship", "kind": "invoice",
    "payer": "program", "status": "paid", "total_cents": 26000,
    "issued_on": "2026-10-01", "due_on": "2026-10-31",
    "submitted_on": "2026-10-02", "paid_on": "2026-10-11",
    "reference": null, "checks_failing": 0
  }
}

Reply with any 2xx status within 8 seconds. Anything else is retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 8 hours and 24 hours (7 tries in all). Deliveries can arrive more than once or out of order, so key on ChoicePaid-Delivery and compare created. Payloads never include names: call the API if you need them. Redirects aren't followed, and endpoints on private or internal networks are refused.

Verifying signatures

The signature header is t=<unix seconds>,v1=<hex>, where the hex is HMAC-SHA256 of <t>.<raw body> using your endpoint's signing secret (starts whsec_). Check it against the raw body before parsing, compare in constant time, and reject timestamps more than five minutes old.

import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyChoicePaid(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const t = Number(parts.t);
  if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false;
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  const a = Buffer.from(expected), b = Buffer.from(parts.v1 ?? "");
  return a.length === b.length && timingSafeEqual(a, b);
}

Program slugs

Students take a program slug:

ProgramSlug
Texas TEFAtexas-tefa
Utah UFAutah-fits-all
Georgia Promise Scholarshipgeorgia-promise
Louisiana GATORlouisiana-gator
Wyoming ESAwyoming-esa
Missouri MOScholarsmissouri-moscholars
Iowa Students Firstiowa-students-first
Arizona ESAarizona-empowerment-scholarship
Florida Step Up For Studentsflorida-step-up
Alabama CHOOSE Actalabama-choose-act
West Virginia Hope Scholarshipwest-virginia-hope
Indiana INESAindiana-esa
Tennessee EFS and ESAtennessee-efs-esa
Arkansas EFAarkansas-efa
North Carolina ESA+north-carolina-esa-plus
South Carolina ESTFsouth-carolina-estf
New Hampshire NH EFAnew-hampshire-efa
Mississippi ESAmississippi-esa
Montana ESAmontana-esa

Questions

Which plans include the API?

Pro and School. API keys and webhooks are managed by the workspace owner in Account, then Developers.

Do webhooks include student names?

No. Webhook payloads carry ids, amounts, dates and statuses only. If your system needs a name, fetch the record through the API with your key, so names only travel over a request you authenticated.

Can the API submit invoices to ClassWallet, Odyssey or a state program?

No. ChoicePaid never submits anything to a program or payment platform. The API changes the status you track in ChoicePaid, the same as clicking the buttons in the app, and drafts must pass their program checks before they can be marked submitted.

What happens if my webhook endpoint is down?

Each delivery is tried up to 7 times over about a day and a half. After 20 failed deliveries in a row the webhook is switched off and the owner gets an inbox notice. You can retry any delivery from the log.

Is there a sandbox?

Create a read-only key and a sample student in your workspace to try requests safely. Sample students and their invoices are left out of API results, so add a real test student if you want to see one in the API.

Something missing? Tell us what you're connecting.