SDI Standard
The key words MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, SHOULD NOT, RECOMMENDED, MAY, and OPTIONAL in this document are to be interpreted as described in RFC 2119 and RFC 8174.
1. Overview
Section titled “1. Overview”Admissions systems and school management systems do not talk to each other. When a school admits an applicant, a staff member re-keys that applicant’s name, contact details, programme, intake, and uploaded documents into the school’s SMS by hand. This is slow, error-prone, and does not scale.
SDI (Student Data Interoperability) is an open standard for moving an admitted applicant from an admissions system into an SMS as a student record, over a small, predictable, RESTful HTTP interface secured with OAuth 2.0. It is designed so that:
- A Provider (the SMS) declares the fields it expects through a machine-readable schema, so the admissions system can present its administrators a precise field-mapping experience instead of guesswork.
- An administrator authorizes the connection once — ideally without ever handling an API key — and the link is then maintained automatically.
- Documents (transcripts, identity documents, photographs, issued letters) transfer securely by reference, never by embedding bytes in a payload.
- The interface versions cleanly and is extensible to capabilities beyond admissions over time.
SDI defines only the wire contract a Provider exposes. How an admissions system decides when to send, which of its internal fields map to a Provider’s schema, or how it queues and retries work, is the Consumer’s concern and is out of scope.
1.1 Conformance levels
Section titled “1.1 Conformance levels”A Provider is SDI-conformant when it implements, at minimum:
- the Discovery document (§5),
- at least one OAuth 2.0 grant defined in §6,
- the
enrollment-synccapability’s schema and ingest endpoints (§8), and - document resolution as a caller (§10) if any record it accepts can carry documents — the Provider fetches document bytes from the Consumer.
Conformance levels and the certification programme are defined in §15.
1.2 What SDI is not
Section titled “1.2 What SDI is not”SDI is not a database, a central data store, a message bus, or a file-transfer network. A Provider keeps its own data store and exposes only the endpoints defined here. SDI does not require the Consumer and Provider to share any infrastructure.
2. Terminology and roles
Section titled “2. Terminology and roles”2.1 Admissions vs. enrollment
Section titled “2.1 Admissions vs. enrollment”These are two distinct lifecycle stages owned by two different systems, and SDI sits on the boundary between them:
- Admissions is the funnel before a person is a student: application, document collection, review, decision (offer / waitlist / reject), and acceptance. The person is a candidate / applicant and is not yet a member of the school. This stage is owned by the admissions system.
- Enrollment is the state in which the person is a registered, active student: they hold a student record and are associated with a class, section, term, or year. This stage is owned by the SMS.
SDI is the handoff at the boundary. When an applicant is admitted (and/or accepts/enrolls) in the admissions system, SDI transfers that person into the SMS so the SMS can create or update the student record. The decision of which status triggers the handoff (e.g. “admitted” vs. “enrolled”) is a Consumer configuration concern and is not encoded in the protocol.
2.2 Roles
Section titled “2.2 Roles”| Role | Definition |
|---|---|
| Provider | The SMS. It hosts the SDI endpoints, acts as an OAuth 2.0 authorization server and resource server, declares the fields it expects, and ingests records. |
| Consumer | The admissions system. It is the OAuth 2.0 client, discovers the Provider, maps its source data to the Provider’s declared schema, and pushes records. |
| Administrator | A human at the school who authorizes and manages a connection between a Consumer and a Provider. |
2.3 Other terms
Section titled “2.3 Other terms”- Capability — a named, independently versioned unit of functionality a Provider may implement (e.g.
enrollment-sync). See §7. - Connection — an authorized, ongoing relationship between one Consumer and one Provider, backed by OAuth 2.0 tokens.
- Discovery document — the machine-readable metadata document a Provider publishes at a well-known URL (§5).
- Record — one applicant/student represented as a JSON object conforming to the Provider’s declared schema, optionally carrying document descriptors.
- Envelope — the protocol-level wrapper around a batch of records submitted to an ingest endpoint.
- Steward — the body that governs SDI: it publishes and versions this specification, runs the conformance and certification programme, holds the certification mark, signs the software statements that anchor registration trust (§6.1.2), and maintains the standard’s registries (e.g. the identifier-scheme registry, §9.3.1).
3. Architecture overview
Section titled “3. Architecture overview”A Consumer integrates with a Provider in four phases: discover, register/authorize, map, and sync.
flowchart LR
subgraph Provider["Provider (SMS)"]
WK[".well-known/<br/>sdi-configuration"]
AS["OAuth 2.0<br/>authorization server"]
SCHEMA["schema_endpoint"]
INGEST["ingest_endpoint"]
PULL["document puller"]
STORE[("Student records")]
end
subgraph Consumer["Consumer (Admissions system)"]
DISC["Discovery client"]
AUTH["OAuth client +<br/>token store"]
MAP["Field-mapping UI"]
SYNC["Sync engine"]
RESOLVE["document resolve<br/>(resolveUrl)"]
FILES[("Object storage")]
end
DISC -->|GET| WK
AUTH -->|register / authorize / token| AS
MAP -->|GET expected fields| SCHEMA
SYNC -->|POST records| INGEST
INGEST --> STORE
PULL -.->|signed-JWT resolve doc id| RESOLVE
RESOLVE -->|short-lived signed URL| PULL
RESOLVE --> FILES
The connection lifecycle:
sequenceDiagram
autonumber
participant A as Administrator
participant C as Consumer
participant P as Provider
A->>C: Enter Provider base URL, click 'Connect'
C->>P: GET /.well-known/sdi-configuration
P-->>C: Discovery document (endpoints, grants, capabilities)
alt Authorization Code + PKCE + Dynamic Registration
C->>P: POST registration_endpoint (self-register client)
P-->>C: client_id (+ secret)
C->>A: Redirect to authorization_endpoint
A->>P: Authenticate and consent
P-->>C: Authorization code (redirect)
C->>P: POST token_endpoint (code + PKCE verifier)
P-->>C: access_token + refresh_token
else Client Credentials (pasted client_id / secret)
A->>C: Paste client_id + client_secret
C->>P: POST token_endpoint (grant_type=client_credentials)
P-->>C: access_token
end
C-->>A: Connection established
4. Transport and conventions
Section titled “4. Transport and conventions”- Transport. All endpoints MUST be served over HTTPS (TLS 1.2 or later). Plaintext HTTP MUST NOT be used for any SDI endpoint.
- Style. Endpoints are RESTful: resource-oriented URLs, standard HTTP methods, and standard status
codes. Requests and responses use
application/jsonunless otherwise stated. - Methods. Reads use
GET. Record submission and document resolution usePOST. A Provider MAY additionally supportPUT/PATCHon its own resources but MUST NOT require them for conformance. - Status codes. A Provider MUST use conventional semantics:
200/201success,202accepted for asynchronous processing,400malformed request,401missing/invalid token,403insufficient scope,404unknown resource,409idempotency conflict,410expired resource,422semantic validation failure,429rate limited,5xxserver error. - Errors. Error responses MUST use the
application/problem+jsonmedia type (RFC 9457) with at leasttype,title, andstatus, and SHOULD includedetailand, where applicable, anerrorsarray of field-level problems. - Idempotency. Any state-changing request (record ingest, document resolution that mints a URL)
MUST honour an
Idempotency-Keyrequest header. A Provider MUST return the original result for a repeated key within its retention window (§12). - Rate limiting. A Provider MAY rate-limit and SHOULD signal limits with
429and aRetry-Afterheader. Consumers MUST honourRetry-After. - Pagination. Any list endpoint a Provider exposes MUST be cursor-paginated and MUST NOT rely on offset paging.
- Time and identifiers. Timestamps MUST be RFC 3339 / ISO 8601 UTC. Identifiers are opaque UTF-8 strings; a Consumer MUST NOT parse meaning from a Provider’s identifiers and vice versa.
4.1 Localization of Provider-supplied text
Section titled “4.1 Localization of Provider-supplied text”Some responses carry human-readable text the Consumer displays to administrators — the title and
description of schema fields (§8.1), enumerated option labels, and the title/detail of
problem+json errors. Because the Consumer cannot pre-translate text the Provider defines, a Provider that
supports more than one language MUST localize this text through standard HTTP content negotiation:
- A Provider MUST advertise the languages it can render in
languages_supportedand name adefault_languagein its discovery document (§5.1). Language tags are BCP-47 and SHOULD use thelanguage-REGIONform (e.g.en-GB,fr-FR,sw-KE). - On any response carrying such text, a Provider MUST honour the request’s
Accept-Languageheader, MUST setContent-Languageto the language it actually returned, and MUST setVary: Accept-Language. When it cannot satisfy the request it MUST fall back todefault_languageand report that fallback viaContent-Language, so the Consumer can detect a mismatch. - Machine-readable identifiers are never localized. Property names, enumerated values, contract
versions, scopes, and the
type/codeof an error are locale-invariant; only the human-facingtitle,description, anddetailchange with language. This lets a Consumer key its own translations off the stablecodeand disregard the Provider’s wording where it maintains its own.
A Provider that renders only one language MAY ignore Accept-Language, but MUST still declare that
language as default_language and set Content-Language on its responses.
5. Discovery
Section titled “5. Discovery”A Provider MUST publish a discovery document as JSON at:
GET https://{provider-base}/.well-known/sdi-configurationThe document is the single entry point: from a Provider’s base URL a Consumer learns every endpoint,
supported grant, scope, capability, and version. It MUST be retrievable without authentication and
SHOULD be cacheable (Cache-Control/ETag).
5.1 Fields
Section titled “5.1 Fields”| Field | Type | Required | Description |
|---|---|---|---|
sdi_version |
string | yes | The SDI protocol version this document describes (e.g. "1.0"). |
protocol_versions |
string[] | yes | All SDI protocol major.minor versions the Provider supports. |
issuer |
string (URL) | yes | The Provider’s stable issuer identifier. |
authorization_endpoint |
string (URL) | conditional | Required if Authorization Code grant is supported. |
token_endpoint |
string (URL) | yes | OAuth 2.0 token endpoint. |
registration_endpoint |
string (URL) | conditional | Required if Dynamic Client Registration is supported. |
jwks_uri |
string (URL) | conditional | Required if the Provider issues or verifies signed JWTs. |
grant_types_supported |
string[] | yes | Subset of authorization_code, client_credentials, refresh_token. |
code_challenge_methods_supported |
string[] | conditional | Required if Authorization Code is supported; MUST include "S256". |
scopes_supported |
string[] | yes | All scopes the Provider recognises (§6.4). |
capabilities |
object[] | yes | The capabilities the Provider implements (§5.2). |
languages_supported |
string[] | no | BCP-47 language-REGION tags the Provider can render human-readable text in (§4.1). Omit or single-entry when not localized. |
default_language |
string | yes | BCP-47 tag of the language returned when a request’s Accept-Language cannot be satisfied (§4.1). |
A Provider MUST ignore unknown top-level fields and MUST NOT fail if a Consumer sends headers or query parameters it does not recognise.
5.2 Capability descriptors
Section titled “5.2 Capability descriptors”Each entry in capabilities describes one capability and the endpoints that serve it:
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | yes | Capability identifier, e.g. "enrollment-sync". |
contract_versions |
string[] | yes | The capability’s data-contract versions the Provider supports (semver). |
schema_endpoint |
string (URL) | yes | Returns the JSON Schema of fields the Provider expects (§8.1). |
ingest_endpoint |
string (URL) | yes | Accepts record batches (§8.2). |
scopes |
string[] | yes | The scopes required to call this capability’s endpoints. |
A Consumer MUST treat a capability it does not recognise as inert (ignore it), and MUST NOT assume any endpoint exists unless the discovery document advertises it.
6. Authentication and authorization
Section titled “6. Authentication and authorization”SDI uses OAuth 2.0 (RFC 6749). The Provider is the
authorization server and the resource server. The Consumer is the client. A Provider MUST support at
least one of the two grants below and MUST advertise its choice in grant_types_supported. Supporting
both is RECOMMENDED.
6.1 Authorization Code + PKCE (interactive)
Section titled “6.1 Authorization Code + PKCE (interactive)”This is the RECOMMENDED primary grant. An administrator authorizes the connection through a browser redirect; no API key is ever handled by the Consumer.
- The Consumer MUST use PKCE (RFC 7636) with the
S256code-challenge method. - The Provider’s
authorization_endpointMUST authenticate the administrator and obtain explicit consent for the requested scopes. - On success the Provider redirects back with an authorization code, which the Consumer exchanges at the
token_endpointfor anaccess_tokenand arefresh_token.
6.1.1 Dynamic Client Registration
Section titled “6.1.1 Dynamic Client Registration”To avoid an administrator hand-copying client credentials between two consoles, a Provider SHOULD
support Dynamic Client Registration (RFC 7591) at
registration_endpoint. The Consumer self-registers (submitting its redirect URIs and requested scopes)
and receives a client_id (and, for confidential clients, a client_secret). When registration is not
supported, an administrator provisions a client manually in the SMS and uses the Client Credentials grant
(§6.2).
6.1.2 Registration security
Section titled “6.1.2 Registration security”Dynamic registration MUST NOT be open and unauthenticated. Registering a client grants no data
access on its own — data access requires authorization (administrator consent for Authorization Code, or
Provider-granted scopes for Client Credentials) — but open registration still enables client-table abuse
and, more seriously, client impersonation (a hostile client registering under a trusted name to phish
an administrator’s consent). A Provider’s registration_endpoint MUST therefore require at least one
of:
- a software statement (RFC 7591 §2.3) — a JWT signed by the SDI steward asserting the Consumer’s verified identity, which the Provider MUST validate against the steward’s published keys; and/or
- an initial access token (RFC 7591 §3) the Provider issues out-of-band, typically minted inside an authenticated administrator action in the SMS (“connect an admissions system”).
The Provider MUST present the verified client identity on the consent screen, SHOULD rate-limit registration, and SHOULD expire clients that never complete an authorization.
6.2 Client Credentials (machine-to-machine)
Section titled “6.2 Client Credentials (machine-to-machine)”This grant supports headless, server-to-server connections. An administrator provisions a client in the
SMS and pastes the client_id and client_secret into the Consumer. The Consumer requests tokens
directly with grant_type=client_credentials. Because a secret is handled directly, a Consumer MUST
store it encrypted at rest (§13) and a Provider SHOULD scope and rate-limit such clients tightly.
6.3 Tokens and re-authentication
Section titled “6.3 Tokens and re-authentication”- Access tokens MUST be short-lived (a
expires_inof one hour or less is RECOMMENDED). - When a
refresh_tokenis issued, the Provider SHOULD apply refresh-token rotation and MUST treat reuse of a rotated (consumed) refresh token as a compromise: it MUST revoke the token family and require re-authorization. - On a
401, a Consumer refreshes and retries. If refresh fails withinvalid_grant(refresh token expired or revoked), the connection enters a needs-reauthorization state: the Consumer MUST pause syncs and prompt an administrator to re-consent (Authorization Code) or supply new credentials (Client Credentials). Client Credentials connections simply request a new token and have no refresh token.
sequenceDiagram
autonumber
participant C as Consumer
participant P as Provider
C->>P: POST ingest_endpoint (access_token)
P-->>C: 401 invalid_token (expired)
alt has refresh_token
C->>P: POST token_endpoint (grant_type=refresh_token, rotated)
alt refresh ok
P-->>C: new access_token (+ new refresh_token)
C->>P: retry POST ingest_endpoint
P-->>C: 200 OK
else refresh rejected
P-->>C: 400 invalid_grant
C->>C: mark connection needs-reauthorization, pause syncs
C->>C: notify administrator to re-consent
end
else client_credentials
C->>P: POST token_endpoint (grant_type=client_credentials)
P-->>C: new access_token
C->>P: retry POST ingest_endpoint
end
6.4 Scopes
Section titled “6.4 Scopes”A Provider MUST define least-privilege scopes per capability and advertise them in scopes_supported
and on each capability descriptor. The following scope identifiers are RESERVED by this standard:
| Scope | Grants |
|---|---|
sdi.enrollment.write |
Submit records to the enrollment-sync ingest endpoint. |
sdi.enrollment.read |
Read enrollment schema and prior submission results. |
A Provider MUST NOT grant a token broader scope than the administrator consented to. Document resolution is authenticated by the Provider’s signed assertion rather than a scoped token (§10).
7. Capability framework
Section titled “7. Capability framework”SDI is organized around capabilities. A capability is a named, independently versioned unit of functionality with its own schema, ingest, and (optionally) document endpoints, advertised in the discovery document (§5.2). This keeps the core — discovery, OAuth, transport conventions, document handling, versioning — generic, so new capabilities can be added without changing the protocol core.
flowchart TB
subgraph Core["SDI core (generic)"]
D["Discovery"]
O["OAuth 2.0"]
T["Transport + errors + idempotency"]
DOC["Document handling"]
V["Versioning"]
end
subgraph Caps["Capabilities"]
E["enrollment-sync (v1)"]
R["applicant-update (future)"]
G["document-issuance (future)"]
ATT["cohort-backfill (future)"]
end
Core --> E
Core --> R
Core --> G
Core --> ATT
This version of the standard defines exactly one capability: enrollment-sync (§8). Future
capabilities are out of scope here (§14).
8. The enrollment-sync capability
Section titled “8. The enrollment-sync capability”enrollment-sync transfers an admitted/accepting applicant into the SMS as a student record. It defines
two Provider endpoints: a field-schema endpoint and an ingest endpoint. Any documents a record references
are resolved by the Provider calling back to the Consumer (§10).
8.1 Field-schema endpoint
Section titled “8.1 Field-schema endpoint”GET {schema_endpoint}Authorization: Bearer {token with sdi.enrollment.read}Accept-Language: sw-KE, en;q=0.8The Provider MUST return a JSON Schema (Draft 2020-12) describing the fields it expects in a record. This is the contract a Consumer maps its source data onto, and the data a Consumer’s field-mapping experience is built from. The schema:
-
MUST declare each expected field with a stable property name, a type, and whether it is
required. Property names are locale-invariant (§4.1). -
SHOULD annotate fields with a human-readable
titleanddescriptionfor display in mapping UIs. -
MUST localize
title/descriptionper the request’sAccept-Languagewhen it supports more than one language, settingContent-Languageon the response and falling back todefault_language(§4.1). -
MAY constrain values (
enum,format,pattern,maxLength) — a Consumer SHOULD surface these constraints to administrators during mapping. -
SHOULD, where an enumerated field needs human-readable option labels, use the JSON Schema
oneOf+const/titlepattern so the stable value (const) stays locale-invariant while the label (title) localizes with the rest of the schema:"sex": {"type": "string","title": "Sex","oneOf": [{ "const": "male", "title": "Male" },{ "const": "female", "title": "Female" }]} -
SHOULD use the SDI canonical field vocabulary (§9.3) for common attributes so that mappings are predictable across Providers, while remaining free to declare Provider-specific fields.
The response MUST be tagged with its contract_version (header SDI-Contract-Version and/or a schema
$id that encodes the version). A Provider MUST keep a previously advertised schema version resolvable
for the deprecation window (§11).
8.2 Ingest endpoint
Section titled “8.2 Ingest endpoint”POST {ingest_endpoint}Authorization: Bearer {token with sdi.enrollment.write}Idempotency-Key: {key}SDI-Contract-Version: {version}Content-Type: application/jsonThe body is an envelope (§9.1) carrying one or more records. The Provider validates each record against
the advertised schema and creates or updates the corresponding student record. The Provider MUST return
a per-record result (§9.2): each record is independently created, updated, or rejected, so a
partial batch can succeed. The Provider MUST NOT fail an entire batch because one record is invalid.
Idempotency is keyed on Idempotency-Key and each record’s recordRef (§9.1): re-submitting the same
record under the same key MUST NOT create a duplicate.
9. Record shape and data contract
Section titled “9. Record shape and data contract”9.1 Envelope
Section titled “9.1 Envelope”{ "sdi_version": "1.0", "capability": "enrollment-sync", "contract_version": "1.0", "records": [ { "recordRef": "consumer-side-stable-id", "data": { "...": "fields conforming to the Provider's schema (§8.1)" }, "documents": [ "...document descriptors (§10)" ] } ]}recordRef(REQUIRED) is a Consumer-stable identifier for the applicant/record. The Provider MUST treat it as the correlation and idempotency key on the record level, and SHOULD return it in results and persist it for future updates.data(REQUIRED) holds the mapped fields and MUST validate against the Provider’s advertised schema forcontract_version.documents(OPTIONAL) is an array of document descriptors (§10).
9.2 Per-record result
Section titled “9.2 Per-record result”The ingest response MUST return one result per submitted record:
{ "results": [ { "recordRef": "consumer-side-stable-id", "outcome": "created", "externalId": "sms-student-id-123", "problems": [] }, { "recordRef": "another-id", "outcome": "rejected", "problems": [ { "pointer": "/data/dateOfBirth", "code": "invalid_format", "detail": "expected YYYY-MM-DD" } ] } ]}outcomeis one ofcreated,updated,rejected, orskipped(idempotent no-op).externalId(REQUIRED forcreated/updated) is the Provider’s identifier for the student record, which a Consumer SHOULD store to support later updates.problemsuses field pointers (RFC 6901 JSON Pointers into the submitted record) so a Consumer can map failures back to specific fields.
9.3 SDI canonical field vocabulary
Section titled “9.3 SDI canonical field vocabulary”To make mappings predictable across Providers, SDI defines a RECOMMENDED vocabulary of field identifiers for common attributes. A Provider’s schema SHOULD use these identifiers where the concept applies, and MAY add Provider-specific fields freely. The vocabulary is grouped:
- Identity / profile —
givenName,middleName,familyName,fullName,preferredName,email,phone,dateOfBirth,sex,nationality,address,nationalId,localIds(array of{ scheme, value }for jurisdiction- or examination-body identifiers). - Admission context —
applicationRef,decision,programme,level,intakeTerm,intakeYear,campus,decisionDate. - Documents — carried in the envelope’s
documentsarray, not as inlinedatafields (§10). - Extensions — a Provider’s schema MAY define an
extensionsobject property for arbitrary Consumer passthrough data that has no canonical field.
The vocabulary is intentionally minimal and locale-neutral, and accommodates jurisdiction-specific
identifiers (e.g. national examination references) through localIds rather than fixed fields. Field
values are not constrained to any regional code set by this standard.
9.3.1 localIds.scheme
Section titled “9.3.1 localIds.scheme”scheme names which identifier system a value belongs to, so a Provider can interpret and store it
correctly. It is a stable, namespaced, lowercase string. The RECOMMENDED convention is
country.authority (ISO 3166-1 alpha-2 country code, then the issuing authority), with a small set of
country-agnostic identifiers permitted:
scheme |
Identifier |
|---|---|
ng.jamb |
Nigeria — JAMB registration number |
ng.waec |
Nigeria — WAEC examination number |
ke.knec |
Kenya — KNEC index number |
ng.nin |
Nigeria — National Identification Number |
za.id |
South Africa — national ID number |
passport |
Passport number (country-agnostic) |
The standard’s steward maintains a registry of well-known schemes; a Provider MUST pass through and
preserve an unrecognised scheme rather than reject it. Example:
{ "scheme": "ng.jamb", "value": "12345678AB" }.
10. Documents
Section titled “10. Documents”Admission records frequently reference files — transcripts, identity documents, photographs, issued offer/acceptance letters. SDI transfers documents by reference, never by embedding bytes. A record carries lightweight descriptors; the documents live in the Consumer, and the Provider fetches the bytes on demand by calling back to the Consumer.
During ingest the Consumer is the client; for documents the Provider is the caller and authenticates
itself to the Consumer (§10.3). The resolve endpoint is the Consumer’s, conveyed per-document via the
descriptor’s resolveUrl.
10.1 Document descriptor
Section titled “10.1 Document descriptor”{ "id": "opaque-document-id", "type": "transcript", "resolveUrl": "https://admissions.example/sdi/documents/resolve", "filename": "transcript.pdf", "contentType": "application/pdf", "size": 482113, "sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"}id(REQUIRED) is the handle the Provider presents back to resolve the document.resolveUrl(REQUIRED) is the Consumer’s resolution endpoint for this document. The descriptor carries no working file URL and no long-lived token.type(REQUIRED) is a Consumer-declared document class (e.g.transcript,identity,photo,offer_letter); Providers SHOULD preserve it but MUST NOT require a fixed vocabulary.sha256(REQUIRED) lets the Provider verify integrity after download.contentType,filename,sizeare REQUIRED metadata.
10.2 Retrieval
Section titled “10.2 Retrieval”To obtain bytes, the Provider POSTs the document id to its resolveUrl, authenticating with a signed
JWT assertion (§10.3):
POST {resolveUrl}Authorization: Bearer {provider-signed JWT assertion}Content-Type: application/json
{ "id": "opaque-document-id" }The Consumer verifies the assertion, maps it to the connection, and responds with a short-lived, single-purpose signed URL:
{ "url": "https://files.example/...signed...", "expiresAt": "2026-06-29T12:34:56Z", "sha256": "9f86..." }The Provider then downloads the bytes from url and MUST verify the sha256 before storing them.
sequenceDiagram
autonumber
participant P as Provider (SMS)
participant C as Consumer
participant S as Object storage
Note over P: record carried document descriptors with id and resolveUrl
P->>C: POST resolveUrl with id and signed JWT assertion
C->>C: verify JWT against Provider jwks_uri from discovery, map to connection
C->>C: authorize, verify scan-clean and retention
C-->>P: returns short-lived signed url, expiresAt, sha256
P->>S: GET the signed url
S-->>P: bytes
P->>P: verify sha256, then store
10.3 Authenticating the resolve call
Section titled “10.3 Authenticating the resolve call”Because the Provider is the caller here, it MUST authenticate itself to the Consumer. The normative
mechanism is a signed JWT assertion (the private_key_jwt pattern,
RFC 7523):
- The Provider signs a short-lived JWT (
iss= its issuer,aud= theresolveUrl, plusjti,iat,exp) with the key published at itsjwks_uriand sends it as a bearer token. - The Consumer MUST verify the signature against the Provider’s
jwks_uri— which it already knows from discovery for this connection — MUST verify thataudmatches its resolve endpoint, and MUST map the verifiedissto the originating connection. It MUST reject expired assertions and SHOULD reject replayedjtivalues.
The aud claim binds the assertion to the resolve endpoint, so it cannot be replayed against any other
endpoint. Identity comes from the verified iss; authorization is enforced separately, so a valid
assertion only ever yields documents belonging to its own connection (§10.4). This keeps every secret out
of the payload and reuses the keys the Provider already maintains as an OAuth authorization server.
10.4 Rules
Section titled “10.4 Rules”- The signed URL MUST be short-lived and SHOULD be single-use. It MUST NOT be embedded in the ingest payload — it is minted only in response to an authenticated resolution call, so it never sits in stored records or logs.
- A Consumer MUST NOT expose a document that has not passed malware scanning; it MAY return
409with a problem document while a scan is pending. - After the Consumer’s retention window, resolution MUST return
410 Gone. - A document
idis bound to the connection that produced it; the Consumer MUST reject a resolution whose verified caller does not match that connection.
11. Versioning
Section titled “11. Versioning”SDI versions at two levels:
- Protocol version (
sdi_version,protocol_versionsin discovery) —MAJOR.MINOR. A Consumer and Provider negotiate the highest mutually supported protocol version from the discovery document. - Capability contract version (
contract_versionsper capability) — semver per capability schema. The contract version in use is carried on the schema response and on every ingest request via theSDI-Contract-Versionheader (and MAY also be negotiated via theAcceptmedia type, e.g.application/vnd.sdi.enrollment-sync.v1+json).
Rules:
- A Provider MUST support every version it advertises and MUST keep a deprecated version resolvable for a deprecation window of at least 180 days after announcing its removal.
- Within a
MAJOR/MAJOR.MINORline, changes MUST be backward compatible (additive fields, relaxed constraints). Breaking changes MUST increment the major version and be advertised as a new entry, never by mutating an existing one. - A Consumer MUST select a version both sides advertise and MUST NOT assume an unadvertised version exists.
12. Reliability semantics
Section titled “12. Reliability semantics”SDI is a push interface; reliability is a shared responsibility with a clear division:
- Idempotency (Provider). A Provider MUST deduplicate on
Idempotency-Key+recordRefand return the original result for repeats within a retention window of at least 24 hours (SHOULD be 7 days). - Partial success (Provider). A batch MUST report per-record outcomes; valid records are persisted even when others are rejected.
- Retries (Consumer). A Consumer MUST retry transient failures (
429,5xx, network) with exponential backoff and MUST reuse the sameIdempotency-Keyacross retries of the same batch. - Ordering. SDI does not guarantee ordering across batches. A Provider MUST treat a record update
for an existing
recordRefas authoritative regardless of arrival order relative to unrelated records. - Triggering and queueing (when to send, how to queue) are Consumer concerns and out of scope.
13. Security
Section titled “13. Security”- Transport. TLS for every endpoint (§4). Tokens MUST travel only in the
Authorizationheader, never in URLs. - Least privilege. Tokens are scoped per capability (§6.4); a Provider MUST reject calls whose token
lacks the required scope with
403. - Token hygiene. Short-lived access tokens; refresh-token rotation with reuse detection (§6.3). A Consumer MUST store client secrets and refresh tokens encrypted at rest.
- Replay protection. State-changing requests are idempotent (§4, §12); a Provider SHOULD reject stale or replayed requests outside the idempotency window.
- PII minimization. A Consumer SHOULD transmit only fields mapped to the Provider’s schema and MUST NOT rely on the Provider to discard over-shared data.
- Documents. Signed URLs are short-lived, single-purpose, scan-gated, scope-checked, and connection-
isolated (§10.3); integrity is verified via
sha256. - Auditability. A Provider SHOULD record the authenticated client, scope, and outcome of every ingest and document-resolution call.
14. Extending SDI
Section titled “14. Extending SDI”New functionality is added as new capabilities (§7), not by changing the core. A future capability declares its own schema and ingest endpoints in the discovery document and is versioned independently.
SDI’s direction is admissions → SMS: the Consumer pushes, the Provider receives. Future capabilities
preserve that direction. Examples reserved for future work — not defined here — include
applicant-update (pushing post-handoff lifecycle changes — a withdrawal, deferral, or corrected detail —
so the SMS record stays current), document-issuance (pushing issued admission documents such as offer or
acceptance letters to the SMS), and cohort-backfill (a bulk historical push of an already-admitted
cohort). A future export profile (a Consumer emitting to other downstream systems) is likewise an
extension point and is out of scope for this version.
Because capabilities are discovered, a Consumer that predates a capability simply ignores it, and a
Provider that implements only enrollment-sync remains fully conformant.
15. Conformance and certification
Section titled “15. Conformance and certification”15.1 Provider conformance
Section titled “15.1 Provider conformance”A Provider is conformant at the enrollment-sync level when it:
- serves a valid discovery document (§5),
- implements at least one OAuth 2.0 grant (§6) with the reserved scopes it advertises,
- serves the
enrollment-syncschema and ingest endpoints with per-record results and idempotency (§8, §9, §12), and - resolves documents as a caller per §10 — fetching bytes from the Consumer with a signed-JWT assertion — if any accepted record may carry documents.
A Provider MUST pass the published conformance test suite for each level it claims.
15.2 Certification
Section titled “15.2 Certification”“SDI Certified” is a stewardship programme: a Provider that passes the conformance suite may be listed in the public SDI Provider directory and use the certification mark under the programme’s terms. The standard itself is open and free to implement; certification and the marks are administered by the standard’s steward. Certification is OPTIONAL for conformance and MUST NOT be a precondition for two systems interoperating.
16. Appendix A — example discovery document
Section titled “16. Appendix A — example discovery document”{ "sdi_version": "1.0", "protocol_versions": ["1.0"], "issuer": "https://sms.example.edu", "authorization_endpoint": "https://sms.example.edu/oauth/authorize", "token_endpoint": "https://sms.example.edu/oauth/token", "registration_endpoint": "https://sms.example.edu/oauth/register", "jwks_uri": "https://sms.example.edu/oauth/jwks", "grant_types_supported": ["authorization_code", "client_credentials", "refresh_token"], "code_challenge_methods_supported": ["S256"], "scopes_supported": ["sdi.enrollment.write", "sdi.enrollment.read"], "languages_supported": ["en-GB", "fr-FR", "sw-KE"], "default_language": "en-GB", "capabilities": [ { "name": "enrollment-sync", "contract_versions": ["1.0"], "schema_endpoint": "https://sms.example.edu/sdi/enrollment-sync/schema", "ingest_endpoint": "https://sms.example.edu/sdi/enrollment-sync/records", "scopes": ["sdi.enrollment.write", "sdi.enrollment.read"] } ]}17. Appendix B — JSON Schemas
Section titled “17. Appendix B — JSON Schemas”17.1 Ingest envelope (normative)
Section titled “17.1 Ingest envelope (normative)”{ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://sdi.admissio.app/schemas/1.0/envelope.json", "type": "object", "required": ["sdi_version", "capability", "contract_version", "records"], "properties": { "sdi_version": { "type": "string" }, "capability": { "type": "string" }, "contract_version": { "type": "string" }, "records": { "type": "array", "minItems": 1, "items": { "type": "object", "required": ["recordRef", "data"], "properties": { "recordRef": { "type": "string", "minLength": 1 }, "data": { "type": "object" }, "documents": { "type": "array", "items": { "$ref": "https://sdi.admissio.app/schemas/1.0/document-descriptor.json" } } } } } }}17.2 Document descriptor (normative)
Section titled “17.2 Document descriptor (normative)”{ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://sdi.admissio.app/schemas/1.0/document-descriptor.json", "type": "object", "required": ["id", "type", "resolveUrl", "filename", "contentType", "size", "sha256"], "properties": { "id": { "type": "string", "minLength": 1 }, "type": { "type": "string", "minLength": 1 }, "resolveUrl": { "type": "string", "format": "uri" }, "filename": { "type": "string" }, "contentType": { "type": "string" }, "size": { "type": "integer", "minimum": 0 }, "sha256": { "type": "string", "pattern": "^[a-f0-9]{64}$" } }}17.3 Example Provider field schema (illustrative, not normative)
Section titled “17.3 Example Provider field schema (illustrative, not normative)”A Provider’s schema_endpoint returns something like the following — its own declared fields, using the
SDI canonical vocabulary where applicable:
{ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://sms.example.edu/sdi/enrollment-sync/schema/1.0", "type": "object", "required": ["givenName", "familyName", "email", "intakeYear"], "properties": { "givenName": { "type": "string", "title": "First name" }, "familyName": { "type": "string", "title": "Last name" }, "email": { "type": "string", "format": "email", "title": "Email" }, "phone": { "type": "string", "title": "Phone" }, "dateOfBirth": { "type": "string", "format": "date", "title": "Date of birth" }, "sex": { "type": "string", "title": "Sex", "oneOf": [ { "const": "male", "title": "Male" }, { "const": "female", "title": "Female" } ] }, "programme": { "type": "string", "title": "Programme" }, "intakeYear": { "type": "integer", "title": "Intake year" }, "localIds": { "type": "array", "items": { "type": "object", "required": ["scheme", "value"], "properties": { "scheme": { "type": "string" }, "value": { "type": "string" } } } }, "extensions": { "type": "object" } }}