CLIP LearnDocs 1.0
Reference

Public API & webhooks

Developer reference for CLIP Learn's read-only REST API v1 and outbound webhooks: minting keys, authentication, endpoints, event payloads, and the HMAC signature.

CLIP Learn exposes a small, read-only REST API and an outbound webhook system so you can pull your tenant's data into other tools (a data warehouse, a CRM, Zapier, a spreadsheet sync) and be notified when things happen (an assignment is marked, an essay result is released). Everything is scoped to a single tenant: an API key can only ever read the data of the tenant that created it, and a webhook only fires for events in its own tenant.

Both keys and webhooks are managed from Faculty → API & webhooks (/faculty/api). Only users with the FACULTY or ADMIN role can open that page or create, revoke, or delete keys and webhooks. There is no self-service API for creating data — every v1 endpoint is a GET.

Base URL

All examples use the production host https://cliplaw.academy. Substitute your own tenant's host if you have been given a different one. Every endpoint below lives under the /api/v1/ path.

Authentication

The API uses a bearer token in the Authorization header. There are no query-string keys, no cookies, and no OAuth.

Authorization: Bearer <your-key>

The server takes the key, hashes it with SHA-256, and looks it up. If the key is unknown, malformed, or has been revoked, the request is rejected. On every successful call the key's Last used timestamp is updated.

ConditionResponse
Valid, active key200 with a JSON body { "data": [...] }
Missing Authorization header401 { "error": "invalid or missing API key" }
Malformed header (not Bearer <key>)401 { "error": "invalid or missing API key" }
Unknown key401 { "error": "invalid or missing API key" }
Revoked key401 { "error": "invalid or missing API key" }

One error for all auth failures

The API deliberately returns the same 401 message whether the key is missing, malformed, unknown, or revoked. It does not tell you which. If you get a 401, check the header format first, then confirm the key has not been revoked on the API page.

Minting and revoking API keys

Keys are created on the API & webhooks page under the API keys section.

Create a key

Go to Faculty → API & webhooks.
In API keys, optionally type a Key name (for example, Zapier sync). This is a label to help you recognise the key later; it does not affect what the key can do.
Click Generate key.
The full key appears once in a highlighted box labelled "Copy this now — it's shown once". Copy it immediately and store it somewhere safe.

The plaintext key is shown exactly once

CLIP Learn stores only a SHA-256 hash of your key, never the key itself. After you leave or refresh the page the full value is gone forever — the table only ever shows the short prefix (for example clip_a1B2c3D…). If you lose a key, revoke it and generate a new one.

Key name field

FieldWhat it doesNotes
Key nameA human label shown in the keys tableOptional. Trimmed and truncated to 80 characters. If left blank, the key is named API key.

Key format. A generated key looks like clip_ followed by a URL-safe base64 string, e.g. clip_qX8f2.... The prefix stored for display is the first 12 characters (clip_ plus the first seven characters of the random part).

The keys table

Each row in the API keys table shows:

ColumnMeaning
NameThe label you gave the key (or API key).
KeyThe stored prefix followed by . The full key is never shown here.
CreatedThe date the key was generated (formatted like 9 Jul 2026).
Last usedThe date the key last authenticated a request, or if never used.
(action)A Revoke link, or the word revoked if it has already been revoked.

If you have no keys yet, the table is hidden entirely and only the Generate key form and the Try it example are shown.

Revoke a key

Find the key's row in the API keys table.
Click Revoke at the end of the row.
Confirm the prompt: "Revoke this API key? Any integration using it will immediately stop working."

Revocation is immediate and permanent — the row's action cell changes to revoked, and any request using that key from that moment on returns 401. There is no "un-revoke"; generate a new key instead. Revoking one key never affects any other key.

Endpoints

All three endpoints are GET, require a valid bearer key, run on the Node.js runtime, and are always dynamic (never cached). Each returns a JSON object with a single data array. Results are automatically filtered to the key's tenant — there is no tenant parameter to pass, and you cannot read another tenant's data.

GET /api/v1/courses

Returns every course belonging to the key's tenant, ordered by title (A→Z).

Response

{
  "data": [
    {
      "id": "clx...",
      "slug": "company-law",
      "title": "Company Law",
      "subtitle": "Foundations of UK company law",
      "published": true
    }
  ]
}
FieldTypeMeaning
idstringThe course's internal identifier.
slugstringThe URL slug for the course.
titlestringThe course title.
subtitlestring | nullThe course subtitle, if set.
publishedbooleanWhether the course is published (visible to learners) or still a draft.

GET /api/v1/learners

Returns the tenant's learners — users with the CANDIDATE role who are members of the tenant. Ordered by name (A→Z), capped at 1000 rows.

Response

{
  "data": [
    {
      "id": "clx...",
      "name": "Amina Yusuf",
      "email": "amina@example.com",
      "createdAt": "2026-05-01T09:12:00.000Z"
    }
  ]
}
FieldTypeMeaning
idstringThe learner's user identifier.
namestringThe learner's display name.
emailstringThe learner's email address.
createdAtstring (ISO 8601)When the learner's account was created.

Only CANDIDATE users are returned

/api/v1/learners returns only users whose role is CANDIDATE. Faculty, admins, and other staff accounts are never included, even if they belong to the tenant. The list is capped at 1000 learners; there is currently no pagination parameter.

GET /api/v1/enrollments

Returns enrolments for all of the tenant's courses. Ordered by start date (most recent first), capped at 2000 rows.

Response

{
  "data": [
    {
      "userId": "clx...",
      "courseId": "clx...",
      "startedAt": "2026-06-10T00:00:00.000Z",
      "expiresAt": "2027-06-10T00:00:00.000Z"
    }
  ]
}
FieldTypeMeaning
userIdstringThe enrolled learner's user id (join to /api/v1/learners).
courseIdstringThe course id (join to /api/v1/courses).
startedAtstring (ISO 8601)When the enrolment began.
expiresAtstring (ISO 8601) | nullWhen the enrolment expires, or null if it never expires.

The response contains only ids and dates — join userId against /api/v1/learners and courseId against /api/v1/courses to get names and titles. The list is capped at 2000 enrolments with no pagination parameter.

Example request

curl https://cliplaw.academy/api/v1/courses \
  -H "Authorization: Bearer <your-key>"

The same header works for /api/v1/learners and /api/v1/enrollments.

Webhooks

Webhooks push a small JSON payload to a URL you control whenever a subscribed event happens in your tenant. They are managed on the same API & webhooks page, under the Webhooks section.

Add a webhook

Go to Faculty → API & webhooks and scroll to Webhooks.
Enter an Endpoint URL — the address CLIP Learn will POST to. It must begin with http:// or https://.
Choose an Event from the dropdown (see the table below). Choose * to receive every event type.
Click Add webhook. A signing secret is generated automatically and the webhook appears in the list below the form.

Webhook form fields

FieldWhat it doesNotes
Endpoint URLThe destination CLIP Learn POSTs events toRequired. Must start with http:// or https:// or the form silently does nothing. Truncated to 500 characters.
EventWhich event type this webhook subscribes toOne of *, assignment.graded, enrollment.created, exam.result. Defaults to *.

Each webhook in the list shows its URL, its subscribed event, the first 12 characters of its signing secret, and — if any deliveries have failed — a failure count (for example 3 failures).

If no webhooks exist yet

When you have no webhooks, the section shows the reminder: "No webhooks yet. Payloads are signed with x-clip-signature (HMAC-SHA256)."

Delete a webhook

Find the webhook in the list.
Click the trash icon on its row.
Confirm: "Delete this webhook? Events will stop being delivered to it."

Deletion is immediate; the webhook stops receiving events at once. There is no per-webhook "pause" toggle in the UI — to stop deliveries, delete the webhook.

Events

EventWhen it firesdata payload fields
assignment.gradedA faculty member marks an assignment submissionsubmissionId, userId, assignment (the assignment title), score, maxScore
exam.resultAn essay submission is reviewed and its result is releasedsubmissionId, userId, topic (the essay question topic), marks, maxMarks
enrollment.created(subscribable, not currently emitted)
*Wildcard — receives every event aboveThe payload of whatever event fired

enrollment.created is selectable but not yet emitted

You can create a webhook subscribed to enrollment.created, but the current build does not dispatch that event anywhere, so such a webhook (and the enrollment.created slice of a * webhook) will not receive anything. The two events actually delivered today are assignment.graded and exam.result. Subscribing to * is the safe way to future-proof against new events being added.

Payload shape

Every delivery is an HTTP POST with a JSON body of this shape:

{
  "event": "assignment.graded",
  "at": "2026-07-12T14:03:00.000Z",
  "data": {
    "submissionId": "clx...",
    "userId": "clx...",
    "assignment": "Problem question — directors' duties",
    "score": 18,
    "maxScore": 25
  }
}
Top-level fieldMeaning
eventThe event type that fired (e.g. assignment.graded). For a * subscription this is the concrete event, never *.
atISO 8601 timestamp of when the event was dispatched.
dataThe event-specific payload from the table above.

An exam.result delivery's data instead contains submissionId, userId, topic, marks, and maxMarks.

Request headers and signature

Each delivery carries these headers:

HeaderValue
content-typeapplication/json
user-agentCLIPLearn-Webhook/1.0
x-clip-eventThe event name (e.g. assignment.graded)
x-clip-signaturesha256=<hex> — an HMAC-SHA256 of the raw request body, keyed with this webhook's secret

The signature is computed as HMAC_SHA256(secret, rawBody), hex-encoded, prefixed with sha256=. To verify a delivery, compute the same HMAC over the exact raw bytes you received (do not re-serialise the JSON) using the webhook's secret, and compare — ideally with a constant-time comparison.

Verification example (Node.js):

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

function verify(rawBody, signatureHeader, secret) {
  const expected = "sha256=" +
    createHmac("sha256", secret).update(rawBody).digest("hex");
  const a = Buffer.from(signatureHeader);
  const b = Buffer.from(expected);
  return a.length === b.length && timingSafeEqual(a, b);
}

Where secret is the full whsec_… value shown when you created the webhook, rawBody is the unparsed request body, and signatureHeader is the value of x-clip-signature.

The signing secret

Each webhook gets its own secret, generated as whsec_ followed by a random URL-safe base64 string. The UI shows only its first 12 characters; the full secret is set at creation. Treat it like a password — anyone who has it can forge signed payloads. If a secret is exposed, delete the webhook and create a new one to rotate it.

Delivery behaviour

  • Fan-out. When an event fires, it is delivered to every active webhook in the tenant that is subscribed to that event or to *. Deliveries happen in parallel.
  • Fire-and-forget. Delivery never blocks or breaks the action that triggered it (grading still succeeds even if your endpoint is down). There are no automatic retries.
  • Success vs failure. A 2xx response resets the webhook's failure count to zero. Any non-2xx response, a timeout, or a connection error increments its failure count, which is displayed next to the webhook.
  • Timeout. Each delivery has a 6-second timeout; slow endpoints are treated as failures.
  • SSRF protection. Delivery URLs are validated and IP-pinned. Requests that resolve to private, loopback, or otherwise internal address ranges are blocked, and redirects are not followed. Point your webhook at a publicly reachable HTTPS endpoint.

Respond fast with a 2xx

Your endpoint should acknowledge with a 2xx status within 6 seconds and do any heavy processing asynchronously afterwards. Because there are no retries, a slow or failing endpoint simply misses that event — the failure count rises but the event is not re-sent.

Permissions summary

ActionWho can do it
View the API & webhooks pageFACULTY or ADMIN
Generate / revoke an API keyFACULTY or ADMIN
Add / delete a webhookFACULTY or ADMIN
Call a /api/v1/* endpointAnyone holding a valid, non-revoked bearer key (scoped to that key's tenant)

Keys and webhooks are always tenant-scoped: they only ever read or fire for the tenant that owns them, and there is no cross-tenant access.

xAPI statements (LRS integration)

CLIP Learn exposes learner activity as xAPI (Experience API) statements, so a Learning Record Store can ingest it.

GET /api/v1/xapi/statements — newest first, authenticated with the same bearer key. Query parameters:

ParameterWhat it does
limitHow many statements to return (default 100, max 500).
sinceAn ISO-8601 timestamp; returns only statements after it, for incremental pulls.

The response is { "statements": [ … ] }, where each statement follows the xAPI shape — an actor (the learner, as an account on this platform), a verb (viewed / completed / answered / asked / messaged), an object (the activity), and a timestamp. Internal events map to standard xAPI verb IRIs (e.g. a completed lesson → http://adlnet.gov/expapi/verbs/completed).

curl "https://cliplaw.academy/api/v1/xapi/statements?limit=50&since=2026-07-01T00:00:00Z" \
  -H "Authorization: Bearer <your-key>"

Statements are tenant-scoped to the key. (This is a read/pull surface; SCORM 2004 and a full LTI story are on the roadmap.)

On this page