API Reference
Digital Skills Assessment API
Version 1.0.0 · Base URL https://www.digitalskillsassessment.co.uk/api/v1
Public REST API for e-portfolio and MIS platforms (Aptem, OneFile, Maytas, Bud, PICS, Smart Assessor and others) to consume assessment results from Digital Skills Assessment.
Integration shape: your platform is the API client. An organisation admin generates an Access Key ID and Secret Access Key in DSA (Organisation Settings), and pastes them into your platform's integration configuration — the same shape used by the incumbent assessment APIs this market already integrates with. No OAuth2.
Polling: results are designed for cheap, frequent, incremental polling (per-minute is fine). Use since/cursor pagination and If-None-Match to receive 304 Not Modified on unchanged polls. Rate limits are per key: 300 requests/minute, with 429 + Retry-After when exceeded.
Funding fields: rqf_level places the learner's current skills level within the RQF level descriptors (ASF funding rules 2026/27 §2.5) and below_level_1 evidences the digital entitlement eligibility gate. Both are first-class response fields.
PII: National Insurance numbers are not stored and never returned. Filter by opaque IDs and your own learner references only — no PII in URLs.
Sandbox: a sandbox environment with seeded fake learners and results is available on request — contact admin@digitalskillsassessment.co.uk.
Filtering (three rules): CSV within a param is OR - different params are AND - unknown params are rejected with 400. Repeated keys (subject=maths&subject=english) are identical to CSV. All filter params are flat snake_case — no filter[x] brackets, no operator suffixes.
Delta sync correctness: subject, assessed_from/assessed_to, rqf_level_code and below_level_1 are DELTA-SAFE (immutable once a result is complete) — combine freely with since. group_id and status are DELTA-UNSAFE (mutable): a record can stop matching them, and absence is indistinguishable from "nothing changed". Mitigations: group membership changes cascade updated_at (the record reappears in the unfiltered feed and the new group's feed), and group_id-filtered polls with since receive scope-exit tombstones (removed: {reason: "left_scope", scope: "group_id"}) for assessments whose learner left the group. Run a weekly unfiltered reconciliation sweep as the authoritative sync — filtered delta feeds are a convenience, not the source of truth.
This reference is generated from the machine-readable OpenAPI 3.1 specification. For a guided start, see the developer overview; for the buyer-facing picture, see integrations.
Authentication
Every request carries two headers, generated by an organisation admin in Organisation → Data & API:
X-Access-Key-IdX-Secret-Access-Key
Endpoints
get/health
Connectivity test
Verifies credentials. Point your platform's 'Test Connection' button here.
Responses
200Credentials valid401Invalid credentials
get/assessments
Incremental assessment results (the critical endpoint)
Unknown query parameters are rejected with 400 unknown_parameter — a misspelt filter never silently returns the full result set. Filter grammar: CSV within a param is OR, different params are AND. See the top-level description for the delta-safe/delta-unsafe split and the weekly reconciliation requirement.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
since | query | string (date-time) | Return results whose updated_at is at or after this position. Amended results are re-fetched; deletions arrive as tombstones (status "deleted"). |
cursor | query | string | Opaque cursor from the previous page's pagination.next. Cursors are bound to the filter set they were minted under — reusing one with different filters returns 400. |
limit | query | integer | |
learner_ref | query | string | Filter to one learner by your own opaque reference (letters, digits, . _ : / -; max 100 chars). Never pass email addresses or names — PII in query strings is rejected with 400. |
subject | query | string | CSV of subjects (OR): digital, english, maths. Delta-safe. |
assessed_from | query | string (date-time) | Results assessed at or after this date. Delta-safe. |
assessed_to | query | string (date-time) | Results assessed at or before this date. Delta-safe. |
rqf_level_code | query | string | CSV of ILR PriorLevel codes (OR): 1 (Entry), 2 (Level 1), 3 (Level 2), 5 (Level 3). No comparison operators — `rqf_level_code=1,2` expresses 'at or below Level 1' completely. Delta-safe. Prefer below_level_1=true for the ASF entitlement gate. |
below_level_1 | query | string: true | false | The ASF digital entitlement eligibility gate, pre-derived — lead with this rather than re-deriving from level codes. Delta-safe. |
status | query | string | CSV (OR): completed, deleted. DELTA-UNSAFE — see the delta sync correctness notes. |
group_id | query | string | CSV of group UUIDs (OR) from /groups; max 20. Matches assessments of the groups' ACTIVE members. DELTA-UNSAFE: with since=, scope-exit tombstones are emitted for learners who left the group — process them, and run the weekly unfiltered reconciliation sweep. |
sort | query | string: updated_at | -updated_at | assessed_at | -assessed_at | Whitelist of four values; '-' prefix for descending (never '+', which decodes to a space). Rejected together with since= — delta feeds are ordered by updated_at by definition. |
Responses
200Page of results (ETag supplied for If-None-Match polling)304Unchanged since the supplied If-None-Match ETag400Invalid or unknown parameter (unknown_parameter, invalid_since, invalid_cursor, invalid_learner_ref, invalid_subject, invalid_assessed_from, invalid_assessed_to, invalid_rqf_level_code, invalid_below_level_1, invalid_status, invalid_group_id, too_many_groups, invalid_sort, sort_with_since)429Rate limited — honour Retry-After
get/assessments/{id}
Single assessment with domain breakdown
Parameters
| Name | In | Type | Description |
|---|---|---|---|
idrequired | path | string (uuid) |
Responses
200The assessment404Not found (also returned for resources belonging to another organisation)
get/assessments/{id}/report.pdf
The evidence artefact (PDF)
Deterministic evidence PDF. X-Report-SHA256 carries the integrity hash stored at first generation.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
idrequired | path | string (uuid) |
Responses
200PDF binary404Not found
get/learners
Incremental learner list
Unknown query parameters are rejected with 400 unknown_parameter; cursors are bound to the query they were minted under.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
since | query | string (date-time) | |
cursor | query | string | |
limit | query | integer |
Responses
200Distinct learners with last-assessed positions and their active groups (groups[])400Invalid or unknown parameter (unknown_parameter, invalid_since, invalid_cursor)
post/learners
Provision a learner into DSA
Creates an assessment invitation for the learner using the organisation's default test type. Consumes credits exactly as dashboard invitations do; education/unlimited plans bypass credits. The optional groups array is upserted by ref on (type, ref) — no group_id lookup round-trip needed. Missing groups are auto-created (unless the organisation has disabled auto-creation), capped at 500 groups per organisation.
Request body
application/json · required
| Field | Type | Description |
|---|---|---|
full_namerequired | string | |
email | string (email) | |
ref | string | Your platform's learner reference — opaque (letters, digits, . _ : / -; max 100 chars). Email addresses and names are rejected: this value later appears in learner_ref filters and /learners/{ref} URLs, and PII in URLs is forbidden. |
groups | array | Groups to place the learner in, upserted by ref. Requires `ref` (membership is keyed on it). |
Responses
201Learner provisioned; response includes the assessment URL402Organisation has no assessment credits409group_limit_exceeded — the organisation already has 500 groups422group_autocreate_disabled or team_group_managed
get/learners/{ref}
Single learner by your reference, with assessment history
Parameters
| Name | In | Type | Description |
|---|---|---|---|
refrequired | path | string |
Responses
200Learner with assessments404Not found
get/webhooks
List registered webhook endpoints
Responses
200Endpoints (secrets never returned)
post/webhooks
Register a webhook endpoint for push delivery
Deliveries carry two HMAC-SHA256 signatures: X-DSA-Signature (legacy, over the raw body) and X-DSA-Signature-256 (recommended, t=<unix seconds>,v1=<hex> where the MAC covers <t>.<raw body> — verify the MAC and reject timestamps outside a 5-minute tolerance to prevent replay). Deliveries also carry X-DSA-Delivery-Id for deduplication, retry with exponential backoff, and dead-letter visibly. The signing secret is returned once. Scoping: omit event_types/group_ids to receive everything — explicit empty arrays are rejected (an empty filter must never silently mean 'deliver nothing'). Event types: assessment.completed (full payload with group_ids), learner.groups.updated (thin payload: {event, learner_ref, added, removed, occurred_at} — re-fetch for details). A subscription scoped by group_ids receives learner.groups.updated when EITHER an added or a removed group matches, so a cohort-scoped subscriber learns of exits.
Request body
application/json · required
| Field | Type | Description |
|---|---|---|
urlrequired | string (uri) | HTTPS endpoint |
event_types | array | Omit for the default (assessment.completed). |
group_ids | array | Omit to receive events for all groups. Ids must be your own groups (422 unknown_group otherwise). |
Responses
201Registered; response includes the one-time signing secret
delete/webhooks/{id}
Remove a webhook endpoint
Parameters
| Name | In | Type | Description |
|---|---|---|---|
idrequired | path | string (uuid) |
Responses
204Removed404Not found
get/groups
List groups
Parameters
| Name | In | Type | Description |
|---|---|---|---|
type | query | string | CSV of group types (OR) |
active | query | string: true | false | |
since | query | string (date-time) | |
cursor | query | string | |
limit | query | integer |
Responses
200Page of groups400Invalid or unknown parameter
post/groups
Create a group
Idempotent on (type, ref): re-posting an existing group returns it with 200. Groups of type 'team' are managed automatically from the organisation's teams and are rejected with 422.
Request body
application/json · required
| Field | Type | Description |
|---|---|---|
refrequired | string | |
namerequired | string | |
typerequired | string |
Responses
200Already existed (idempotent upsert)201Created409group_limit_exceeded422team_group_managed
get/groups/{id}
Group detail with active member count
Parameters
| Name | In | Type | Description |
|---|---|---|---|
idrequired | path | string (uuid) |
Responses
200The group404Not found
patch/groups/{id}
Rename or archive a group
Archive (active=false) rather than delete — membership history is funding-audit evidence and is never destroyed. 'team' groups reject modification (422).
Parameters
| Name | In | Type | Description |
|---|---|---|---|
idrequired | path | string (uuid) |
Request body
application/json · required
| Field | Type | Description |
|---|---|---|
name | string | |
active | boolean |
Responses
200Updated404Not found422team_group_managed
post/groups/{id}/members
Add members (idempotent)
Adds learners by your own refs. Learners already in the group are reported in already_members, not errored. Membership changes cascade updated_at onto the learners' assessments and emit learner.groups.updated webhook events.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
idrequired | path | string (uuid) |
Request body
application/json · required
| Field | Type | Description |
|---|---|---|
learner_refsrequired | array |
Responses
200{ added: [...], already_members: [...] }404Not found
post/groups/{id}/members/remove
Remove members
Removal takes a body (never DELETE /members/{ref}) so learner refs stay out of URLs. Removal is soft — the membership history is retained for point-in-time attribution. Triggers scope-exit tombstones in group_id-filtered delta feeds and learner.groups.updated webhook events.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
idrequired | path | string (uuid) |
Request body
application/json · required
| Field | Type | Description |
|---|---|---|
learner_refsrequired | array |
Responses
200{ removed: [...], not_members: [...] }404Not found
Schemas
Assessment
| Field | Type | Description |
|---|---|---|
assessment_id | string (uuid) | |
status | string: completed | deleted | completed = live result. deleted = tombstone: the record was withdrawn; remove it from your system. Tombstones carry only assessment_id, status and updated_at. |
updated_at | string (date-time) | Sync position. Changes whenever the record is amended or deleted — poll with since= against this field. |
learner | object | |
assessed_at | string (date-time) | |
subject | string: digital | english | maths | |
framework | string | |
rqf_level | string, nullable | Placement within the RQF level descriptors (ASF §2.5) |
rqf_level_code | integer, nullable | ILR PriorLevel code semantics: Entry Level=1, Level 1=2, Level 2=3, Level 3=5 |
below_level_1 | boolean, nullable | ASF digital entitlement eligibility gate |
working_level | string, nullable | |
recommended_next_level | string, nullable | Supports the ASF enrol-above-assessed-level rule |
is_borderline | boolean | The placement sits close to a level boundary: a 95% confidence interval crossed the deciding rule's threshold. Review alongside wider evidence before funding decisions. |
needs_review | boolean | The two scoring engines (rule-based and IRT) placed this result a full level or more apart; the reconciled level was used. Human review recommended. |
domains | array of object | |
evidence_pack_url | string | The evidence PDF for this assessment |
report_sha256 | string, nullable | SHA-256 of the evidence PDF, stored at first generation |
groups | array of GroupSummary | The learner's active groups. Poll unfiltered and fan out client-side on these — most integrations never need group_id filtering at all. |
Pagination
| Field | Type | Description |
|---|---|---|
limit | integer | |
has_more | boolean | |
next | string, nullable | Absolute URL of the next page; null when exhausted |
Error
| Field | Type | Description |
|---|---|---|
error | object |
Tombstone
| Field | Type | Description |
|---|---|---|
assessment_id | string (uuid) | |
status | string: deleted | |
updated_at | string (date-time) |
GroupSummary
| Field | Type | Description |
|---|---|---|
id | string (uuid) | |
ref | string | Your own group reference (course code, cohort label) |
name | string | |
type | string |
Group
| Field | Type | Description |
|---|---|---|
id | string (uuid) | |
ref | string | |
name | string | |
type | string | Open enum — clients must tolerate unknown values. `team` groups are managed automatically from the organisation's teams and cannot be created or modified via the API. |
active | boolean | |
created_at | string (date-time) | |
updated_at | string (date-time) |
ScopeExitTombstone
| Field | Type | Description |
|---|---|---|
assessment_id | string (uuid) | |
updated_at | string (date-time) | |
removed | object |
A sandbox with seeded learners and results is available on request: admin@digitalskillsassessment.co.uk.