Integration Contract

What a client must know before calling the DOCiD API

Scope

The rest of this documentation describes the schema — what a DOCiD record contains. This page describes the contract — how to reach it, what you are allowed to see, and what the API guarantees.

Read this page first if you are building an integration. The behaviours documented here (particularly the authorization matrix) are not visible from the field definitions alone, and several of them will change what your client receives without changing any HTTP status code.

Environments

DOCiD runs on three deployments. They do not share a database: a DOCiD that resolves on one will generally not exist on another.

Environment Base URL Use for
Production https://docid.africapidalliance.org Live integrations and citable identifiers
Core https://docid-core.africapidalliance.org The deployment that physically hosts the historical uploads
Demo https://docid-demo.africapidalliance.org Evaluation and integration testing

Demo is not a sandbox for identifiers

The demo deployment mints live Handles in the production 20.500.14351 namespace. Identifiers created there are real and permanent. Do not use it for throwaway load or fuzz testing that mints records.

All API paths are prefixed /api/v1, with two deliberate exceptions: the verification endpoint and the DOCiD root resolver, both documented in their own pages. Request and response bodies are application/json; deposit and edit endpoints accept multipart/form-data because they carry files.

Authentication

Authentication is a bearer JWT obtained from the login endpoint.

POST /api/v1/auth/login
{
  "email": "researcher@example.ac.ke",
  "password": "..."
}
200 OK (abridged)
{
  "status": true,
  "message": "Login successful",
  "token": "eyJhbGciOiJIUzI1NiIs...",
  "refresh_token": "eyJhbGciOiJIUzI1NiIs...",
  "user_id": 143,
  "user_name": "...",
  "email": "researcher@example.ac.ke"
}

The access token is returned as token, not access_token

This trips up clients written against the OAuth convention. The refresh token is named refresh_token. Present the access token as Authorization: Bearer <token>.

Access tokens expire after 24 hours by default (deployment-configurable). Exchange a refresh token at POST /api/v1/auth/refresh. The token's identity claim is the user id as a string — compare it as such when matching ownership.

Social login (Google, GitHub, ORCID) authenticates existing accounts only. Since the change of August 2026 it no longer auto-creates accounts: an unrecognised social identity returns a payload indicating registration is required rather than silently provisioning a user.

Authorization matrix

Several endpoints return different content to different callers with the same 200 status. A client that tests only for success will not notice.

Capability Anonymous Authenticated Record owner
Resolve a DOCiD, read metadata Yes Yes Yes
Read national-ID creator identifiers Masked Masked Full value
Read checksums (/api/v1/verify) Yes Yes Yes
Deposit a publication No Yes Yes
Edit or delete a record No Owner only Yes
Create a new version of a record No Owner only — 403 otherwise Yes
List available resource types Full list Filtered if the account category is restricted Same as authenticated

National-ID masking

Where a creator carries identifier_type: "national_id", the identifier is returned masked — first and last character preserved, the rest replaced with asterisks — to every caller except the owner of the record presenting a valid JWT. Masked entries carry identifier_masked: true.

Same creator, two callers
// anonymous, or any non-owner
{ "identifier": "4*******8", "identifier_type": "national_id", "identifier_masked": true }

// owner, with Authorization: Bearer <token>
{ "identifier": "455765838", "identifier_type": "national_id" }

Masking applies to the publication detail endpoints, both DOCiD resolvers, and the DOI resolver. Values of two characters or fewer are returned unchanged, since masking them would convey nothing. Do not cache a masked value and later treat it as the identifier.

Account-category resource-type filtering

An account may belong to a category flagged restricted. Such an account may only use the resource types explicitly mapped to its category — and the mapping is an allowlist even when empty, so a restricted category with no mappings can use none.

Read and write paths behave differently, deliberately:

Path No category / unrestricted Anonymous Unresolvable identity
Reading the resource-type list Full list Full list (backward compatible) Full list
Depositing or versioning Unrestricted Cannot reach these paths — a JWT is required Denied — fail-closed

The asymmetry is intentional: an anonymous reader browsing the vocabulary is harmless, while a valid token whose user no longer exists must never be treated as unrestricted.

Pagination

List endpoints take page (1-based, default 1) and page_size (default 10), and return the envelope alongside the results:

Pagination envelope
{
  "publications": [ ... ],
  "total": 3598,
  "page": 1,
  "page_size": 10,
  "total_pages": 360
}

Note the parameter is page_size, not the more common per_page or limit.

Errors and status codes

Status Meaning in DOCiD
200 Success. Content may still be redacted — see the authorization matrix
400 Malformed request — a missing identifier, or an identifier failing a route's format rule
401 Missing, malformed, or expired token
403 Authenticated but not permitted — most often versioning or editing a record you do not own, or a resource type outside your account category
404 No such record
410 Retired. The DOCiD existed and has been soft-deleted. Identifiers are permanent, so a retired record returns 410 rather than 404 — treat it as "was here, deliberately withdrawn", and do not purge your local copy's identifier
500 Server error

Error bodies carry an error key with a human-readable message. Do not parse those messages; branch on the status code.

Stability and reporting problems

The /api/v1 prefix denotes the current stable surface. Fields may be added to responses without notice, so clients must tolerate unknown keys. Fields will not be removed or have their meaning changed within v1.

Administrative and internal endpoints exist but are deliberately not documented here; they are not part of the integration surface and may change without notice.

Where this documentation and the API disagree, the API is authoritative and the documentation is a bug. Report discrepancies to the African PID Alliance so they can be corrected — the v2.0 revision of these pages exists because a year of schema changes went unrecorded.