Partner API, version 1

Put assessments inside your own website

Show our practice papers in your pages, or run tests you write yourself: your server starts a sitting, the candidate takes it in your page, and the result comes back to you.

API address
https://www.supergb.com/cbt/partner-api/v1

How it fits together

  1. Create keys and add your websites in the console.
  2. For practice papers, paste the snippet into your page.
  3. For your own tests, your server starts a sitting and receives a launch address.
  4. Your page opens it, the candidate sits the test, and the result reaches you by webhook or from the API.

Keys

pk_test_ pk_live_
Publishable keys go in your web pages. They only open practice papers on the websites you listed.
sk_test_ sk_live_
Server keys act for your organisation. Keep them on your server and never send them to a browser.

Test keys work straight away and are not counted against your plan. Live keys need your account to be approved.

Practice papers

Add the script once, then mount a paper wherever you want it. The frame resizes itself to fit its content.

<div id="gb-test"></div>
<script src="https://www.supergb.com/cbt/js/embed/v1.js"></script>
<script>
  GreenBridge.mount("#gb-test", { key: "pk_live_...", paper: "PAPER_ID" });
</script>

The paper is the id shown in the snippet builder in your console. Add mode: "exam" for timed exam conditions.

Your own tests

Write tests in your console under Tests and open them. When a candidate is ready, your server starts a sitting:

curl -X POST https://www.supergb.com/cbt/partner-api/v1/sessions \
  -H "Authorization: Bearer $GREEN_BRIDGE_KEY" \
  -H "Content-Type: application/json" \
  -d '{"test_id": "TEST_ID", "language": "fr", "candidate": {"id": "learner-42", "name": "Ada Obi", "email": "ada@example.com"}}'

candidate.id is your own id for the person, up to 128 characters. Name and email are optional; we keep only what you send.

language is optional: one of de, en, es, fr, yo, ig, ha, nl, pt, sw. The sitting's buttons, timer and messages appear in that language; your questions stay as you wrote them. Without it the sitting is in English.

The answer carries a launch address that works once, within 10 minutes. Open it in your page:

HTTP/1.1 201 Created

{
  "attempt_id": "att_...",
  "launch_token": "lt_...",
  "launch_url": "https://www.supergb.com/cbt/fr/embed/v1/test?launch=lt_...",
  "launch_expires_at": "2026-10-23T09:10:00Z",
  "live": true,
  "language": "fr"
}
<div id="gb-test"></div>
<script src="https://www.supergb.com/cbt/js/embed/v1.js"></script>
<script>
  GreenBridge.mountTest("#gb-test", {
    launchUrl: "LAUNCH_URL_FROM_YOUR_SERVER",
    onEvent: function (event) {
      if (event.type === "submitted") {
        showThanks(event.detail.attempt_id);
      }
    }
  });
</script>

Once opened, a sitting lasts up to 24 hours and a refresh resumes it. Each candidate sits each test once.

To list your tests and their ids:

curl https://www.supergb.com/cbt/partner-api/v1/tests -H "Authorization: Bearer $GREEN_BRIDGE_KEY"

Messages from the frame

onEvent receives these, and only from our frame:

EventMeaning
readyThe page has loaded.
resizeThe content changed height. The frame follows it on its own.
completedA practice paper was marked. detail carries score and max_score.
submittedA test was submitted. detail carries attempt_id. Fetch the result from your server, not from the browser.

Results

Submitted attempts, newest first, 50 to a page. Filter by test_id, candidate_id or both.

curl "https://www.supergb.com/cbt/partner-api/v1/attempts?test_id=TEST_ID&page=0" -H "Authorization: Bearer $GREEN_BRIDGE_KEY"
curl https://www.supergb.com/cbt/partner-api/v1/attempts/ATTEMPT_ID -H "Authorization: Bearer $GREEN_BRIDGE_KEY"

An attempt looks like this. questions is included on the Growth plan and above.

{
  "id": "att_...",
  "test_id": "TEST_ID",
  "candidate_id": "learner-42",
  "live": true,
  "language": "fr",
  "started_at": "2026-10-23T09:02:11Z",
  "submitted_at": "2026-10-23T09:41:57Z",
  "integrity_events": 0,
  "score": 34.0,
  "max_score": 40.0,
  "percent": 85.0,
  "grading_status": "fully_graded",
  "questions": [
    {"question_id": "q1", "type": "multiple_choice_question", "score": 1.0, "max_score": 1.0}
  ]
}

grading_status is fully_graded once nothing is left to mark by hand, otherwise pending_manual_review or pending_transcription.

Webhooks

Add up to 5 https addresses in your console. Each event is sent as a signed POST and retried for 3 days until your server answers with a 2xx status.

EventMeaning
attempt.startedThe candidate opened the sitting.
attempt.submittedThe candidate submitted.
attempt.gradedEvery mark is final. Sent again whenever a mark changes.
attempt.integrity_flaggedThe candidate left the test or tried to copy or paste 3 times.
pingA test event sent from your console.
POST /hooks/green-bridge
Content-Type: application/json
Green-Bridge-Event: attempt.graded
Green-Bridge-Delivery: whd_...
Green-Bridge-Signature: t=1792834917,v1=5f0c...

{
  "id": "evt_graded_att_..._1",
  "type": "attempt.graded",
  "created_at": "2026-10-23T10:21:57Z",
  "data": { "id": "att_...", "candidate_id": "learner-42", "score": 34.0, "max_score": 40.0, "grading_status": "fully_graded" }
}

An event id never changes, so you can safely ignore one you have already handled.

Checking the signature

Compute HMAC-SHA256 of t, a full stop and the raw body, using your signing secret, and compare it with v1. Refuse a timestamp more than five minutes old.

Node.js

const crypto = require("crypto");

function isFromGreenBridge(header, rawBody, secret) {
  const parts = Object.fromEntries(header.split(",").map(part => part.split("=")));
  if (!parts.t || !parts.v1 || Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) {
    return false;
  }
  const expected = crypto.createHmac("sha256", secret).update(parts.t + "." + rawBody).digest("hex");
  return expected.length === parts.v1.length
    && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}

Python

import hashlib, hmac, time

def is_from_green_bridge(header, raw_body, secret):
    parts = dict(part.split("=", 1) for part in header.split(","))
    if "t" not in parts or abs(time.time() - int(parts["t"])) > 300:
        return False
    signed = parts["t"].encode() + b"." + raw_body
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts.get("v1", ""))

Errors

Errors come back as JSON with a stable code:

HTTP/1.1 404 Not Found

{"error": {"code": "test_not_found"}}
Code HTTP Meaning
invalid_key 401 The key is missing, unknown or revoked.
partner_suspended 403 Your account is suspended. Contact us.
live_keys_need_approval 403 Live keys work once your account is approved.
not_on_plan 403 Your plan does not include this.
test_not_found 404 No open or closed test of yours has that id.
attempt_not_found 404 No attempt of yours has that id.
candidate_not_found 404 You have no candidate with that id.
test_not_open 409 The test is not open yet, or has closed.
already_submitted 409 This candidate has already submitted this test.
invalid_request 400 A field is missing or not in the expected form.
monthly_limit_reached 429 This month's allowance is used up. Test keys still work.
internal_error 500 Something went wrong on our side. Try again.

Limits and data

  • Up to 300 requests a minute for each server key. Beyond that the answer is 429 with no body; wait a minute and retry.
  • Live sittings and AI marking count against your plan's monthly allowance. Sittings started with a test key are not counted.

Your data

What we keep about your tests and candidates, for how long, and how to get it back.

Tests and your questions
Kept until you delete them or close your account.
Candidates
The id you send, and a name and email only if you send them. Kept for a year after the candidate was last seen.
Sittings and results
Answers, recordings, marks and integrity events. Kept for a year after the sitting started, then erased.
Practice papers
Kept for a year, only to count your usage. The score reaches your page in the completed event.
Webhook deliveries
Kept for 30 days, in the delivery log of your console.
API usage
Daily counts per key, kept for 400 days, on the Usage screen of your console.

Fetch results at any time with GET /attempts. On the Growth and Institution plans you can also download each test's results as a CSV from the test's menu in your console: one row per sitting with every question's mark, and the candidate's answer on the questions you wrote. Questions from the Green Bridge bank show their marks only.

To erase one candidate sooner:

curl -X POST https://www.supergb.com/cbt/partner-api/v1/candidates/erase \
  -H "Authorization: Bearer $GREEN_BRIDGE_KEY" \
  -H "Content-Type: application/json" \
  -d '{"candidate_id": "learner-42"}'

A result a candidate saves to their own Green Bridge account belongs to them from then on. Erasing the candidate or closing your account does not remove it.

Your account's owner can close it under Websites and keys. Keys and webhooks stop at once and everything above is erased within a day, so download what you need first.

The commitments behind this are in Partners and Data Processing in our Terms and Conditions.

Need something the plans do not cover? Use Contact us on the Plan screen of your console.