responsiblo

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:

  1. We switch the API on for your company; in your administration you create an API key in the tab API.
  2. When a client should be trained, your system calls POST /v1/users with your identifier of the client.
  3. We create a profile without an e-mail and answer an entry link (entryUrl); you show it to the client as a button.
  4. The client clicks it and is in the course at once.
  5. 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"}'
FieldRequiredMeaning
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/..."
}
statusMeaning
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.

EventWhen
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" } }
HTTPreasonWhat 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