Developers
Partner API
Send a person to a responsiblo program under your own pseudonymous identifier - no e-mail, no name, no other personal data - and read back the state of the education. Use it as a compliance check before a contract, a deposit or a payout.
Version 1.1.0 · JSON over HTTPS · OpenAPI
How it works
The API is the same for every partner. In five steps:
- We switch the API on for your company; in your administration you create an API key in the tab API.
- When a client should be trained, your system calls POST /v1/users with your identifier of the client.
- We create a profile without an e-mail and answer an entry link (entryUrl); you show it to the client as a button.
- The client clicks it and is in the course at once.
- You read the state with GET /v1/users/{identifier} whenever you need it, or we tell your server through a webhook.
You are the controller of your identifier, we process pseudonymous data only. Never send an e-mail address or a name as the identifier - the API refuses an e-mail with the error 422.
Address and authentication
Every call goes over HTTPS to the address below and carries your API key in the header Authorization.
https://api.responsiblo.com Authorization: Bearer rsb_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
- rsb_live_... opens your production program, rsb_test_... your sandbox program.
- A key is shown once, when it is created; we keep only its fingerprint. Store it as a secret on your server - never in a browser or an app.
- A key carries scopes: read (the state of persons) and write (sending persons). It may be limited to your IP addresses and have an expiry.
- Rotation: you create a new key, the old one keeps working for 7 days, then it expires.
- Answers are never cached (Cache-Control: no-store) and the API takes no calls from a browser (no CORS).
POST /v1/users
Sends a person to the program of the key (scope write).
curl -X POST https://api.responsiblo.com/v1/users \
-H "Authorization: Bearer $RSB_KEY" \
-H "Content-Type: application/json" \
-d '{"identifier":"client-48213","language":"cs","label":"campaign-2026-10"}'
| Field | Required | Meaning |
|---|---|---|
identifier |
yes | Your pseudonym of the person, 1 to 128 characters A-Z a-z 0-9 . _ : @ -; never an e-mail address (refused with 422). |
program |
no | The code of the program; when given, it must be the program of the key. |
language |
no | The language of the course (cs, en, ...); by default the first language of the program. |
label |
no | Your label - a cohort or a campaign, 1 to 64 characters, never personal data. |
expiresAt |
no | The last day of the access, YYYY-MM-DD. |
201 Created for a new person, 200 OK when the identifier is already there - nothing changes, the call is safe to repeat. The body is the state below, with entryUrl. The limit of persons or of the licence of the program answers 403.
GET /v1/users/{identifier}
The state of a person (scope read): what a compliance check needs, nothing more.
curl https://api.responsiblo.com/v1/users/client-48213 -H "Authorization: Bearer $RSB_KEY"
{
"identifier": "client-48213",
"program": "your-program",
"status": "in_progress",
"progress": 43,
"lessonsCompleted": 3,
"lessonsTotal": 7,
"quizScore": null,
"label": "campaign-2026-10",
"createdAt": "2026-10-20T10:02:11Z",
"startedAt": "2026-10-20T10:05:40Z",
"completedAt": null,
"lastActivityAt": "2026-10-20T10:31:40Z",
"expiresAt": null,
"certificate": null,
"entryUrl": "https://your-academy.responsiblo.com/entry/..."
}
status | Meaning |
|---|---|
enrolled |
sent, never entered |
started |
entered, nothing completed yet |
in_progress |
lessons completed, the program not yet |
completed |
the program is completed (completedAt) |
expired |
the access ended (expiresAt passed; a sandbox person ends after 7 days) |
quizScore is the result of the final quiz in % once it is finished. Moments are UTC with whole seconds. certificate is reserved and null in version 1.1.
?include=detail
Adds the progress of every course and the result of every quiz - never the single answers.
"courses": [{"id": "733db340-...", "name": "Introduction", "progress": 40, "lessonsCompleted": 2, "lessonsTotal": 5, "completed": false}],
"quizzes": [{"id": "d2e72fd7-...", "name": "Quiz of the introduction", "score": 85, "completedAt": "2026-10-20T10:31:40Z"}]
The entry link
Show entryUrl to the person (a button Start the course in your system). It signs the person into the course - no e-mail, no password.
The link holds for 30 days and may be used again; every GET /v1/users/{identifier} answers a fresh one. Never send it to anybody else: whoever has the link enters as that person.
Webhooks
In the tab API of your administration you set one webhook per program: a public HTTPS address of your server and the events. We then send a POST with JSON - identifiers and states only, never the entry link.
| Event | When |
|---|---|
user.enrolled |
the person was sent (the first time your webhook knows it) |
user.started |
the person entered the course |
user.completed |
the person completed the program |
user.expired |
the access of the person ended |
POST https://your-server.example/responsiblo/webhook
Content-Type: application/json
X-Responsiblo-Event: user.completed
X-Responsiblo-Delivery: 5b0c1f9e-2d1a-4a57-9a3e-0d6c9c1f2b77
X-Responsiblo-Signature: t=1760954400,v1=3f1c...e9
{
"id": "5b0c1f9e-2d1a-4a57-9a3e-0d6c9c1f2b77",
"type": "user.completed",
"createdAt": "2026-10-20T10:40:00Z",
"apiVersion": "1.1.0",
"program": "your-program",
"data": {
"identifier": "client-48213",
"status": "completed",
"progress": 100,
"lessonsCompleted": 7,
"lessonsTotal": 7,
"quizScore": 85,
"label": "campaign-2026-10",
"startedAt": "2026-10-20T10:05:40Z",
"completedAt": "2026-10-20T10:39:12Z",
"expiresAt": null
}
}
Signature
Every message carries X-Responsiblo-Signature: t=<unix time>,v1=<HMAC-SHA256 of the text t.body with the secret of the webhook, hex>. The secret (whsec_...) is shown once when the webhook is created. Check the signature and the age of t before you trust a message; use X-Responsiblo-Delivery as the key of idempotency.
// PHP: $secret = "whsec_...", $header = X-Responsiblo-Signature, $body = the raw body
preg_match('/^t=(\d+),v1=([0-9a-f]{64})$/', $header, $m);
$valid = $m
&& abs(time() - (int) $m[1]) <= 300
&& hash_equals(hash_hmac('sha256', $m[1] . '.' . $body, $secret), $m[2]);
Delivery and retries
A delivery is an answer 2xx within 10 seconds. Otherwise we try again after 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours, then we give up. Redirects are not followed.
20 failed deliveries in a row switch the webhook off until you switch it on again; the messages of the time between wait. The button Test sends a ping; the log of the deliveries is in your administration.
Errors
Every error has one shape: status, code, reason and message; a 422 adds errors (field - what is wrong).
{ "status": "error", "code": 422, "reason": "invalidBody", "message": "The body is not valid.",
"errors": { "identifier": "identifier must be a pseudonym of the partner, never an e-mail address" } }
| HTTP | reason | What to do |
|---|---|---|
| 400 | invalidJson |
send one JSON object |
| 401 | missingToken, invalidToken |
send a valid key (a revoked and an expired key look the same); the header WWW-Authenticate says Bearer |
| 403 | scopeDenied, ipNotAllowed, apiDisabled, partnerInactive, spaceUnavailable, httpsRequired, userLimitReached, seatsExhausted |
the key may not do this, the API is off for your company, or the program is full |
| 404 | userNotFound, notFound |
no person under this identifier in the program of the key |
| 409 | identifierTaken |
the identifier belongs to a person of another program |
| 422 | invalidBody |
see errors |
| 429 | rateLimited |
wait Retry-After seconds |
| 500 | internalError |
try again later; quote incidentId to our support |
Limits
120 calls a minute per key. After 30 refused keys from one address in ten minutes the address waits 10 minutes.
Status and contract
GET /v1/status- no key, answers {"status":"ok","version":"1.1.0"}GET /openapi- no key, the OpenAPI 3 document of the API
Sandbox
A test key rsb_test_... opens your sandbox program with the same course. Persons there end after 7 days; webhooks of the sandbox program are set apart from the production ones.
Former interface
GET /v1/user/{identifier} (the full export with every answer of the quiz) keeps working with the JWT of your partner key, and now with an API key of the scope read too.
Changelog
- 1.1.0 (2026-10) - API keys, POST /v1/users, GET /v1/users/{identifier} with ?include=detail, the entry link, webhooks, one shape of errors, public status and OpenAPI, limits.
- 1.0.0 (2024) - GET /v1/user/{identifier} with the JWT of a partner key.
Do you want the API for your company? Write to us