Skip to content
CursareDocs
Sign in

Start here

  • Overview
  • Getting started
  • Platform concepts

Create

  • Content
  • Editor and blocks
  • Block reference

Publish & access

  • Publishing and access
  • Custom domain & white-label

Learners

  • Learner experience
  • Learners and enrollments
  • Progress and certificates

Teams & cohort

  • Teams
  • Cohorts

Sales

  • Offers & checkout
  • Payments & payouts
  • Affiliates
  • Insights and reports

Admin

  • Organization & security
  • Plans, trial & billing
  • Appearance
  • Members & roles
  • Audit log

Developer

  • Integrations
  • API keys & webhooks
  • MCP server
  • API reference
Explore the API
Developer

API keys & webhooks

The Developer area holds the credentials and callbacks that let external systems talk to your organization: API keys for the /api/v1 REST API and webhooks that push signed events to your endpoints. Managing either one requires an organization owner or admin role.

API keys

API keys grant programmatic access to the organization's /api/v1 REST API. Each key is scoped to the organization that owns it.

  • cr_live_ prefix — the full token has the form cr_live_<secret>. The server stores only a SHA-256 hash and a short 12-character prefix; a database leak never returns a working key.
  • Shown only once — the plaintext token exists only at creation time and cannot be recovered afterward. Copy it right away.
  • Optional expiration — a key can have an expiresAt or never expire; an expired key authenticates as invalid.
  • Revocation (soft) — revoking marks the key as revoked but keeps the row for auditing; a revoked key stops authenticating immediately.
  • Last used — each successful authentication stamps lastUsedAt, useful for spotting idle keys.

The API uses the token as a Bearer; it resolves the organization that owns the key and operates within it.

A school session is not an API credential

The OAuth exchange used to return a learner to a school is internal, single-use, and consumed before the school session is created. Its token is explicitly rejected by /api/v1 and MCP. Developer access still requires a cr_live_… key or the separate MCP OAuth flow described below.

Keys are managed at /dashboard/settings/developer/api-keys.

Never expose a key

Don’t include cr_live_… keys in content, frontend code, or screenshots. If a key leaks, revoke it and create another.

Webhooks

Webhooks deliver signed events to your HTTPS endpoints, with a delivery history (the ‘calls’). Endpoints are managed at /dashboard/settings/developer/webhooks.

Available events

EventTrigger
enrollment.createdA new enrollment is created
content.completedA learner completes the content
purchase.createdA purchase is recorded
purchase.refundedA purchase is refunded
enrollment.requestedA learner requests enrollment, or a pay-first checkout creates a pending request
enrollment.approvedA pending enrollment request is approved
enrollment.rejectedA pending enrollment request is rejected (a pay-first buyer is refunded)
enrollment.expiredA paid request passes its 30-day decision deadline
learner.unenrolledA learner is removed from a content
content.created / content.updated / content.deletedContent lifecycle or public/commercial settings change
content.publishedA content is published
review.created / review.updatedA learner posts or updates a review
cohort.created / .updated / .deletedA cohort's lifecycle changes
cohort.joinedA learner voluntarily joins a cohort
cohort.member.moved / .removedStaff moves or removes a learner
discussion.post.createdA member posts a question or reply in a cohort
discussion.post.deleted / .resolved / .reopenedA question or reply is deleted, resolved, or reopened
offer.createdAn offer is added to a course
offer.updated / offer.primary_changedOffer terms change or another offer becomes primary
offer.archivedAn offer is archived (stops selling)
purchase.installment.paidA monthly installment on a payment plan is paid
purchase.installment.failed / .recovered / .canceledThe plan becomes past due, recovers, or is canceled
campaign.redeemedA campaign code is consumed on a completed purchase

How delivery works

  • Signing secret — each webhook receives a whsec_… secret, generated on the server and shown only once at creation.
  • Envelope and idempotency — every body includes id, event, version, createdAt, organizationId, and data. The same id and body are preserved across attempts; deduplicate by id.
  • HMAC signature — the JSON POST includes x-cursare-event, x-cursare-event-id, x-cursare-event-version, x-cursare-timestamp, and x-cursare-signature (HMAC-SHA256 of the body with the secret). Validate it before trusting the payload.
  • Timeout and resilience — each attempt has a 5-second limit. Events live in a transactional outbox and unsuccessful deliveries retry with exponential backoff, up to eight attempts. Delivery is at least once, so receivers must be idempotent.
  • Delivery history — each attempt records the response status, body (truncated), error, success, and duration, queryable as the endpoint’s history of ‘calls’.
  • Manual test (ping) — you can send a test event (webhook.test) to an endpoint and see the attempt in the history.
  • Enable/disable and delete — inactive endpoints don’t receive the fan-out; only those that are active and subscribed to the event are triggered.

A webhook can be assigned to a team within the organization, via teamId, to group endpoints by owner (personal member or team) in the listing.

At least one event, a valid URL

A webhook needs a valid HTTPS URL and at least one subscribed event. A provided teamId must belong to the same organization.

Local outbox worker

PostgreSQL is the event source of truth. The local adapter tries to process just after the HTTP response, while GET /api/cron/process-events runs an hourly contingency reconciliation using the same CRON_SECRET as the other cron jobs. Moving to Inngest or Trigger.dev only requires another task adapter; producers and handlers keep using serializable IDs and never import the provider SDK.

REST API

The /api/v1 API manages almost everything the dashboard does. The exact request and response schemas are generated from the same contracts that run the API — see the interactive API reference (in production on its own host, docs.cursare.com/api). This section is the map: how the API behaves and which resources it covers.

Conventions

Every call shares the same rules:

  • Authentication — Authorization: Bearer cr_live_…. A key resolves to one organization and acts with org-admin authority scoped to it; reaching another organization's data is impossible.
  • Rate limits — 240 reads/min and 60 writes/min per key. Every response carries X-RateLimit-Limit, -Remaining, and -Reset; a 429 adds Retry-After.
  • Pagination — list endpoints take page and pageSize (capped). A page past the end returns an empty array, so you can loop page++ until it's empty.
  • Money — amounts are integer minor units (e.g. cents) paired with a currency. Amounts in different currencies are never summed into one number.
  • Errors — 400 validation, 401 bad key, 403 not permitted, 404 not found (a resource in another org reads as not-found), 429 rate-limited. The body is { "error": "…" }.
  • Request bodies are JSON, capped at 4 MB.
  • Kind and authoring profile — content payloads carry kind (course or material); slug is null for materials, which have no public page or offers. The stored kind also selects the document profile: a course contains coursePresentation, a material-only courseCurriculum, and course recommendations in courseRelated; a material contains the full learning blocks. GET /document-schema?kind=… documents each profile; document mutations ignore any caller-supplied kind. Creating a course also creates its primary Standard offer; promote and demote change the kind.
  • Offer, enrollment, and purchase — offer.isPrimary is the sole source of the primary offer; there is no content.primaryOfferId. learner.offerId, learnerRequest.offerId, and purchase.offerId are required. Paid learners and pay-first requests also carry the exact purchaseId; free/comped rows keep it null. learner.status alone authorizes access, and a payment event may change it only while its purchase still backs that learner. Uniqueness remains (userId, contentId): several offers or historical purchases never create parallel enrollments.
  • Cohort on the enrollment — every cohort belongs to exactly one offer. learner.cohortId and learnerRequest.cohortId, when present, must target a cohort of that same offer/course pair; learner.cohortRole stores member or star. There is no parallel membership table. offer.deliveryMode is self_paced or cohort, and offer.discussionEnabled controls discussion for that offer's cohorts.

What you can manage

ResourceEndpoints (prefix /api/v1)What you can do
OrganizationGET,PATCH /organization · GET /membersRead/update the organization profile and list staff with their roles
Custom domainGET,PUT,DELETE /organization/custom-domain · POST …/verifyConnect a domain, fetch the DNS records, run the ownership check, disconnect
Organization configurationGET /organization/shapeRead the organization structure used by automation
Lookup & governanceGET /resolve · GET /audit-events · GET /api-keys · DELETE /api-keys/{id}Resolve human names/slugs/emails to IDs, inspect the audit trail, list and revoke credentials
ContentsGET,POST /contents · GET,PATCH,DELETE /contents/{id} · POST /contents/{id}/publish · POST /contents/{id}/duplicate · POST /contents/{id}/promote · POST /contents/{id}/demoteList (paginated + filters), create, edit pricing/theme/access/format/ownership, publish, duplicate the packaging, and change kind (promote a material to a course, turn a course into a material)
DocumentGET /document-schema · GET,PUT /contents/{id}/documentInspect the authoring format, read the draft/published document, or replace the draft
OffersGET /offers · GET,POST /contents/{id}/offers · GET,PATCH /offers/{id} · POST /offers/{id}/duplicate · …/set-primary · …/archiveA course's sellable packages — price, format, enrollment terms; create, update, make primary, duplicate, archive
LearnersGET /learners · GET,POST /contents/{id}/learners · PATCH,DELETE …/{userId} · POST …/{userId}/reset · POST …/learners/reset · GET …/{userId}/masteryList/search, enroll, update metadata, unenroll, inspect mastery, and reset progress individually or in bulk
Enrollment requestsGET /contents/{id}/enrollment-requests · GET /enrollment-requests/summary · POST /enrollment-requests/{id}/approve · …/rejectRead per-course queues or the org-wide summary, approve, or reject with automatic pay-first refund
CohortsGET,POST /contents/{id}/cohorts · PATCH,DELETE /cohorts/{id} · members sub-routesCreate/manage an offer's cohorts (dates, seats, one tutor), assign/move/remove learners through enrollment, set the star badge
Cohort discussionGET,POST /cohorts/{id}/discussion · POST /discussion/{postId}/resolve · …/pin · DELETE /discussion/{postId}Read a cohort's Q&A threads (with replies), post/reply as staff or the tutor, and moderate (resolve, pin, delete)
Intake formsGET,POST /intake-forms · GET,PATCH,DELETE /intake-forms/{id}Manage reusable enrollment-question sets
CampaignsGET,POST /campaigns · PATCH,DELETE /campaigns/{id}Discount codes — per-course or global
TeamsGET,POST /teams · GET,PATCH,DELETE /teams/{id} · member and member-role sub-routesTeams, memberships, and each member's content-domain roles
Sales & purchasesGET /sales · GET /purchases · GET /purchases/{id} · GET /contents/{id}/purchases · POST /purchases/{id}/refundThe order ledger (filter by content, buyer, status, date) and refunds
PayoutsGET /payoutsActual transfers grouped by currency
AffiliatesGET,PATCH /affiliate-program · GET /affiliates · approve/block/commission/settle sub-routesConfigure the native affiliate program, moderate affiliates, override commission, and settle balances
InsightsGET /insights · GET /insights/series · GET /contents/{id}/insights · …/salesHeadline numbers, time series, and per-content academic + sales figures
ReviewsGET /contents/{id}/reviewsRead ratings and the review feed (read-only)
IntegrationsGET /integrations · Google/WhatsApp/video/payment sub-routesInspect the four typed capabilities, run Google OAuth and managed GTM review, update WhatsApp, create direct video uploads, or disconnect providers
MCP connectionsGET /mcp/connections · DELETE /mcp/connections/{clientId}/{userId}List and revoke organization-bound OAuth assistant grants without exposing tokens
WebhooksGET,POST /webhooks · PATCH,DELETE /webhooks/{id} · …/deliveries · …/testRegister and manage the endpoints described above, inspect the delivery log, send a test

What is deliberately not in the API

An org key can't touch personal authentication (login, passwords, 2FA, profiles), impersonation, the organization's billing, or API-key creation (a key that mints keys makes revocation meaningless). It can't act as a learner either — writing progress, quiz attempts, reviews, or votes is bound to the enrolled person to prevent forgery, and checkout runs through the buyer's browser. These stay in the dashboard on purpose.

Refunding expired paid requests on Vercel

Pay-first requests have a 30-calendar-day decision window. The deadline is neither a column nor a per-offer setting: it is always derived from learnerRequest.createdAt + 30 days. Free requests have a null purchaseId and never enter this process.

Operational itemValue
Configurationapps/web/vercel.json — the Vercel project uses apps/web as its Root Directory
Schedule0 5 * * * — one daily run at 05:00 UTC
EndpointGET /api/cron/expire-enrollment-requests
AuthenticationAuthorization: Bearer <CRON_SECRET> sent by Vercel
SecretCRON_SECRET is required in Production, random, and at least 16 characters long
EnvironmentsVercel schedules the cron only on Production deployments; Preview and local development do not trigger it automatically
Response{ expired, scanned, refunded, failed } for inspection in the function logs

GET /contents/{id}/enrollment-requests exposes expiresAt (null for a free request) and refundPending. When refundPending is true, API clients must not offer manual actions; the approval and rejection endpoints return 400 to preserve the durable retry queue.

Ordering and failure recovery

  1. approveRequest blocks a paid request as soon as it reaches 30 days, independently of the cron job.
  2. The cron marks overdue paid requests expired, then selects every expired or rejected request whose purchase has no refundedAt and is not canceled.
  3. It calls PaymentProvider.refund first, using purchase.id as the idempotency key.
  4. Only after provider confirmation does it call markRefunded: it records refundedAt, removes the request from work without deleting its history, adjusts the coupon, and publishes purchase.refunded.
  5. The buyer email is best-effort and happens after the money operation; an email failure never rolls back the refund.
  6. If the provider or database fails before completion, the request remains visible as Refund pending and is retried the next day with the same key.

Do not delete a Refund pending request

A terminal request combined with a null purchase.refundedAt is the durable retry queue. Investigate failed and the Vercel function logs; do not alter either fact manually. Stripe implements automatic refunds. An external integration must implement refund or complete the return in its own provider.

MCP server

Cursare ships an MCP server (Model Context Protocol) so AI clients — Codex, ChatGPT, Claude, Cursor, and any MCP-compatible agent — can operate your organization directly. The endpoint is https://mcp.cursare.com. To connect Codex with a browser login:

codex mcp add cursare --url https://mcp.cursare.com
codex mcp login cursare

Two auth modes are available: OAuth (the server acts as the signed-in user, with their real team roles — editor, finance, cohort, learners) or an org API key (cr_live_… from Settings → Developer → API keys, org-admin authority, headless). The full guide — every tool, connection steps per client, authoring workflow, and protocol details — lives at MCP server.

Best practices

  • Record who created each key or endpoint, which environment it uses, and how to revoke the credential.
  • Treat every key and every webhook secret as sensitive: shown once, stored as a hash/cipher, never in the frontend or screenshots.
  • Test in the appropriate environment whenever you rotate a key or point a webhook at a new endpoint.
API referenceEndpoints, Bearer authentication, and the API v1 schemas.IntegrationsPayment, video, and embed connections for the organization.
PreviousIntegrationsNextMCP server

Your privacy choices

Necessary cookies keep Cursare working. Analytics and marketing technologies are optional and stay off until you choose them. Privacy Policy.

Sign-in, security and core platform functions.

Audience measurement such as Google Analytics.

Tag Manager and campaign attribution.