API Specification
The contract between two teams: resources, errors, versioning and what happens when it breaks.
Last updated
An API specification is a contract, so it must be precise about the parts people argue over later: error semantics, idempotency, pagination, versioning and deprecation. The schema itself belongs in OpenAPI; this document holds the decisions the schema cannot express.
When to use it
- Before another team, or an external partner, writes code against your service.
- When a public or partner-facing interface needs a support and deprecation commitment.
- Alongside — not instead of — a machine-readable OpenAPI or protobuf definition.
What it must answer
- What resources exist, and what operations are allowed on each?
- How does a caller authenticate, and what is it authorised to do?
- What errors can occur, how are they shaped, and which are retryable?
- What are the rate limits, the pagination rules, and the versioning policy?
Template
API Specification: <Service> v<major>
| Field | Value |
|---|---|
| Base URL | |
| Machine-readable spec | link to openapi.yaml |
| Owner team | |
| Support channel | |
| Stability | Experimental / Stable / Deprecated |
1. Purpose and audience
Who calls this, for what.
2. Authentication and authorisation
Scheme, token lifetime, scopes, and what each scope permits.
3. Conventions
Media types, date and money formats, casing, null vs absent, time zones,
correlation header.
4. Resources
<Resource>
| Method | Path | Purpose | Idempotent? | Required scope |
|---|
Request and response examples for each non-trivial operation.
5. Errors
| HTTP | Code | Meaning | Retryable? | Caller action |
|---|
Error body shape, and the rule for machine-readable codes.
6. Idempotency and concurrency
Idempotency keys, optimistic concurrency (ETag / version), replay window.
7. Pagination, filtering, sorting
8. Rate limits and quotas
Limits, headers returned, behaviour at the limit.
9. Versioning and deprecation
How breaking changes are made, notice period, sunset headers.
10. Non-functional commitments
Latency targets, availability, and where they are measured.
11. Changelog
# API Specification: <Service> v<major>
| Field | Value |
|---|---|
| Base URL | |
| Machine-readable spec | link to openapi.yaml |
| Owner team | |
| Support channel | |
| Stability | Experimental / Stable / Deprecated |
## 1. Purpose and audience
Who calls this, for what.
## 2. Authentication and authorisation
Scheme, token lifetime, scopes, and what each scope permits.
## 3. Conventions
Media types, date and money formats, casing, null vs absent, time zones,
correlation header.
## 4. Resources
### <Resource>
| Method | Path | Purpose | Idempotent? | Required scope |
|---|---|---|---|---|
Request and response examples for each non-trivial operation.
## 5. Errors
| HTTP | Code | Meaning | Retryable? | Caller action |
|---|---|---|---|---|
Error body shape, and the rule for machine-readable codes.
## 6. Idempotency and concurrency
Idempotency keys, optimistic concurrency (ETag / version), replay window.
## 7. Pagination, filtering, sorting
## 8. Rate limits and quotas
Limits, headers returned, behaviour at the limit.
## 9. Versioning and deprecation
How breaking changes are made, notice period, sunset headers.
## 10. Non-functional commitments
Latency targets, availability, and where they are measured.
## 11. Changelog
Worked example
API Specification: Provisioning API v1
| Field | Value |
|---|---|
| Base URL | https://provisioning.internal.example.com/v1 |
| Machine-readable spec | /openapi.yaml |
| Owner team | Platform Identity |
| Stability | Stable since 2026-09-01 |
1. Purpose and audience
Lets internal callers create and track access provisioning requests, and lets
line managers approve entitlements that fall outside a joiner’s standard role
bundle. Its consumers are the service desk tooling, the People Operations
admin UI, and the manager approval app. It is not exposed outside the
corporate network and is not intended for bulk reporting — use the daily audit
export for that.
2. Authentication and authorisation
OAuth 2.0 client credentials. Tokens live 15 minutes. Scopes:
| Scope | Permits |
|---|---|
| provisioning.read | Read requests and their status |
| provisioning.write | Create requests, re-resolve pending requests |
| provisioning.approve | Approve or reject non-standard entitlements |
Approval requires a user-delegated token; a service token with
provisioning.approve is rejected, because approval must be attributable to a
person (audit driver D1).
3. Conventions
JSON only (application/json). Timestamps are RFC 3339 in UTC with a trailingZ. Field names are snake_case. Absent means “not supplied”; null means
“explicitly cleared” — the two are not interchangeable on PATCH. Every request
should carry X-Correlation-Id; if absent, the service mints one and returns
it in the response.
4. Resources
Provisioning requests
| Method | Path | Purpose | Idempotent? | Scope |
|---|---|---|---|---|
| POST | /requests | Create a provisioning request | Yes, with Idempotency-Key | provisioning.write |
| GET | /requests/{id} | Fetch one request | Yes | provisioning.read |
| GET | /requests | List, filtered | Yes | provisioning.read |
| POST | /requests/{id}/approve | Approve pending entitlements | Yes, with If-Match | provisioning.approve |
| POST | /requests/{id}/reject | Reject pending entitlements | Yes, with If-Match | provisioning.approve |
| POST | /requests/{id}/re-resolve | Re-resolve against the current bundle | No | provisioning.write |
POST /requests
{
"employee_id": "E-40219",
"job_code": "ENG-3",
"start_date": "2026-10-05",
"manager_employee_id": "E-11804",
"additional_entitlements": ["repo:payments:write"]
}
201 Created
{
"id": "req_01J9F3K2",
"state": "AWAITING_APPROVAL",
"bundle_version": "eng-3@17",
"entitlements": [
{"name": "sso:default", "source": "bundle", "state": "PENDING"},
{"name": "repo:payments:write", "source": "additional", "state": "AWAITING_APPROVAL"}
],
"created_at": "2026-09-18T09:12:44Z"
}
5. Errors
Body shape:
{"code": "bundle_not_found", "message": "...", "correlation_id": "...", "details": {}}
| HTTP | Code | Meaning | Retryable? | Caller action |
|---|---|---|---|---|
| 400 | invalid_request | Schema or field validation failed | No | Fix the request |
| 401 | unauthenticated | Missing or expired token | After refresh | Get a new token |
| 403 | insufficient_scope | Token lacks the scope, or is a service token on /approve | No | Use a delegated token |
| 404 | not_found | Request ID unknown | No | — |
| 409 | state_conflict | Request is not in a state that allows this action | No | Re-read and decide |
| 412 | precondition_failed | If-Match did not match current version | No | Re-read, re-apply |
| 422 | bundle_not_found | Job code has no bundle | No | Route to People Ops |
| 429 | rate_limited | Quota exceeded | Yes, after Retry-After | Back off |
| 503 | target_unavailable | A downstream target system is down | Yes | Retry with backoff; request is queued regardless |
code values are stable and part of the contract; message is human-readable
and may change without notice. Never branch on message.
6. Idempotency and concurrency
POST /requests accepts Idempotency-Key; the same key with the same body
returns the original 201 for 24 hours. The same key with a different body
returns 409 idempotency_key_reuse. Approve and reject require If-Match with
the request’s ETag, so two managers cannot decide simultaneously.
7. Pagination, filtering, sorting
GET /requests is cursor paginated. Pass limit (default 50, max 200) andcursor from the previous response’s next_cursor; a null next_cursor
means the last page. Offset paging is not supported, because requests change
state while a caller walks the list and offsets would skip or repeat rows.
Filters: state, employee_id, job_code, created_after, created_before.
Repeated values for one filter are OR-ed, different filters are AND-ed.
Sorting is fixed at created_at descending; it is not a client choice, which
keeps the cursor implementation honest.
8. Rate limits and quotas
600 requests per minute per client. Responses carry RateLimit-Limit,RateLimit-Remaining and RateLimit-Reset. Bulk import is exempt but capped
at one concurrent job per client.
9. Versioning and deprecation
The major version is in the path. Additive changes (new optional fields, new
enum values on output) ship within v1 — clients must ignore unknown fields and
tolerate unknown enum values. Breaking changes require v2, with both versions
served for at least 6 months. Deprecated endpoints return Deprecation andSunset headers for the whole notice period.
10. Non-functional commitments
p95 < 300 ms for reads and < 800 ms for writes, measured at the service’s
ingress. 99.5% monthly availability. These are the numbers the owning team is
paged against.
11. Changelog
| Version | Date | Change |
|---|---|---|
| 1.14.0 | 2026-11-12 | Added POST /requests/{id}/re-resolve and RateLimit-* headers; bundle_version is now always present |
| 1.12.0 | 2026-10-01 | Deprecated X-Request-Trace in favour of X-Correlation-Id |
| 1.9.0 | 2026-09-01 | Declared stable; no breaking change will ship inside v1 |
| 1.0.0 | 2026-07-14 | First release |
# API Specification: Provisioning API v1
| Field | Value |
|---|---|
| Base URL | https://provisioning.internal.example.com/v1 |
| Machine-readable spec | /openapi.yaml |
| Owner team | Platform Identity |
| Stability | Stable since 2026-09-01 |
## 1. Purpose and audience
Lets internal callers create and track access provisioning requests, and lets
line managers approve entitlements that fall outside a joiner's standard role
bundle. Its consumers are the service desk tooling, the People Operations
admin UI, and the manager approval app. It is not exposed outside the
corporate network and is not intended for bulk reporting — use the daily audit
export for that.
## 2. Authentication and authorisation
OAuth 2.0 client credentials. Tokens live 15 minutes. Scopes:
| Scope | Permits |
|---|---|
| provisioning.read | Read requests and their status |
| provisioning.write | Create requests, re-resolve pending requests |
| provisioning.approve | Approve or reject non-standard entitlements |
Approval requires a user-delegated token; a service token with
provisioning.approve is rejected, because approval must be attributable to a
person (audit driver D1).
## 3. Conventions
JSON only (`application/json`). Timestamps are RFC 3339 in UTC with a trailing
`Z`. Field names are `snake_case`. Absent means "not supplied"; `null` means
"explicitly cleared" — the two are not interchangeable on PATCH. Every request
should carry `X-Correlation-Id`; if absent, the service mints one and returns
it in the response.
## 4. Resources
### Provisioning requests
| Method | Path | Purpose | Idempotent? | Scope |
|---|---|---|---|---|
| POST | /requests | Create a provisioning request | Yes, with Idempotency-Key | provisioning.write |
| GET | /requests/{id} | Fetch one request | Yes | provisioning.read |
| GET | /requests | List, filtered | Yes | provisioning.read |
| POST | /requests/{id}/approve | Approve pending entitlements | Yes, with If-Match | provisioning.approve |
| POST | /requests/{id}/reject | Reject pending entitlements | Yes, with If-Match | provisioning.approve |
| POST | /requests/{id}/re-resolve | Re-resolve against the current bundle | No | provisioning.write |
POST /requests
```json
{
"employee_id": "E-40219",
"job_code": "ENG-3",
"start_date": "2026-10-05",
"manager_employee_id": "E-11804",
"additional_entitlements": ["repo:payments:write"]
}
```
201 Created
```json
{
"id": "req_01J9F3K2",
"state": "AWAITING_APPROVAL",
"bundle_version": "eng-3@17",
"entitlements": [
{"name": "sso:default", "source": "bundle", "state": "PENDING"},
{"name": "repo:payments:write", "source": "additional", "state": "AWAITING_APPROVAL"}
],
"created_at": "2026-09-18T09:12:44Z"
}
```
## 5. Errors
Body shape:
```json
{"code": "bundle_not_found", "message": "...", "correlation_id": "...", "details": {}}
```
| HTTP | Code | Meaning | Retryable? | Caller action |
|---|---|---|---|---|
| 400 | invalid_request | Schema or field validation failed | No | Fix the request |
| 401 | unauthenticated | Missing or expired token | After refresh | Get a new token |
| 403 | insufficient_scope | Token lacks the scope, or is a service token on /approve | No | Use a delegated token |
| 404 | not_found | Request ID unknown | No | — |
| 409 | state_conflict | Request is not in a state that allows this action | No | Re-read and decide |
| 412 | precondition_failed | If-Match did not match current version | No | Re-read, re-apply |
| 422 | bundle_not_found | Job code has no bundle | No | Route to People Ops |
| 429 | rate_limited | Quota exceeded | Yes, after Retry-After | Back off |
| 503 | target_unavailable | A downstream target system is down | Yes | Retry with backoff; request is queued regardless |
`code` values are stable and part of the contract; `message` is human-readable
and may change without notice. Never branch on `message`.
## 6. Idempotency and concurrency
POST /requests accepts `Idempotency-Key`; the same key with the same body
returns the original 201 for 24 hours. The same key with a *different* body
returns 409 `idempotency_key_reuse`. Approve and reject require `If-Match` with
the request's ETag, so two managers cannot decide simultaneously.
## 7. Pagination, filtering, sorting
`GET /requests` is cursor paginated. Pass `limit` (default 50, max 200) and
`cursor` from the previous response's `next_cursor`; a null `next_cursor`
means the last page. Offset paging is not supported, because requests change
state while a caller walks the list and offsets would skip or repeat rows.
Filters: `state`, `employee_id`, `job_code`, `created_after`, `created_before`.
Repeated values for one filter are OR-ed, different filters are AND-ed.
Sorting is fixed at `created_at` descending; it is not a client choice, which
keeps the cursor implementation honest.
## 8. Rate limits and quotas
600 requests per minute per client. Responses carry `RateLimit-Limit`,
`RateLimit-Remaining` and `RateLimit-Reset`. Bulk import is exempt but capped
at one concurrent job per client.
## 9. Versioning and deprecation
The major version is in the path. Additive changes (new optional fields, new
enum values on output) ship within v1 — clients must ignore unknown fields and
tolerate unknown enum values. Breaking changes require v2, with both versions
served for at least 6 months. Deprecated endpoints return `Deprecation` and
`Sunset` headers for the whole notice period.
## 10. Non-functional commitments
p95 < 300 ms for reads and < 800 ms for writes, measured at the service's
ingress. 99.5% monthly availability. These are the numbers the owning team is
paged against.
## 11. Changelog
| Version | Date | Change |
|---|---|---|
| 1.14.0 | 2026-11-12 | Added `POST /requests/{id}/re-resolve` and `RateLimit-*` headers; `bundle_version` is now always present |
| 1.12.0 | 2026-10-01 | Deprecated `X-Request-Trace` in favour of `X-Correlation-Id` |
| 1.9.0 | 2026-09-01 | Declared stable; no breaking change will ship inside v1 |
| 1.0.0 | 2026-07-14 | First release |
Common mistakes
- Documenting only the happy path. Consumers spend most of their integration effort on errors. The error table is the most-read section of any API document.
- No retryability guidance. Without it, callers either retry nothing (and drop work) or retry everything (and duplicate it).
messageused as a machine-readable code. State explicitly which field is stable, or clients will parse the prose and break on a typo fix.- Versioning policy left unstated. “We will not break you” is not a policy. Notice period and sunset mechanics are.
- Two sources of truth. If this document and the OpenAPI file disagree, generate what you can from the schema and keep only the decisions here.
Related templates
- Solution Architecture Document — the integration section this expands.
- Technical Design Document — the internals behind the contract.
- Release Notes — where deprecations are announced.
- Test Plan — contract tests are derived from this document.
- Architecture & Design — the other four architecture and design templates.
Linked from
- Release Notes — What changed, what breaks, and what the reader has to do about it.
- Software Requirements Specification — System-level functional and non-functional requirements, each one testable.
- Solution Architecture Document — The structure of a system, the constraints that shaped it, and the trade-offs accepted.
Last updated 23 Aug 2026, 17:59 -0500.