How it fits together
- Create keys and add your websites in the console.
- For practice papers, paste the snippet into your page.
- For your own tests, your server starts a sitting and receives a launch address.
- 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:
| Event | Meaning |
|---|---|
ready | The page has loaded. |
resize | The content changed height. The frame follows it on its own. |
completed | A practice paper was marked. detail carries score and max_score. |
submitted | A 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.
| Event | Meaning |
|---|---|
attempt.started | The candidate opened the sitting. |
attempt.submitted | The candidate submitted. |
attempt.graded | Every mark is final. Sent again whenever a mark changes. |
attempt.integrity_flagged | The candidate left the test or tried to copy or paste 3 times. |
ping | A 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.