Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
5924700
Add RFC 089: Identifiers API
kenoir Jun 18, 2026
45c0463
RFC 089: reframe caching around database cost, not consumer quota
kenoir Jun 18, 2026
a3c5d19
RFC 089: language pass (remove em dashes, soften phrasing)
kenoir Jun 18, 2026
46c3375
RFC 089: drop the decision log, rename "The contract" to "API Contract"
kenoir Jun 18, 2026
2099c1c
RFC 089: rename "API contract (OpenAPI)" to "OpenAPI specification"
kenoir Jun 18, 2026
87b7c38
RFC 089: render oneOf/anyOf response bodies in openapi.md
kenoir Jun 22, 2026
30be607
RFC 089: clarify the 400 enum case names the type parameter
kenoir Jun 22, 2026
e2c9202
RFC 089: 'rides' -> 'reads' for the forward-lookup index access
kenoir Jun 22, 2026
70274aa
RFC 089: fix item-level predecessor direction (Sierra number -> FOLIO…
kenoir Jun 22, 2026
f053339
RFC 089: settle the type enum question (scope to three, extend on dem…
kenoir Jun 22, 2026
ba64d52
RFC 089: name the two expected clients in the caching hit-ratio open …
kenoir Jun 24, 2026
20ccec9
RFC 089: fold specific-sibling include into the bare-value reverse op…
kenoir Jun 24, 2026
59aacdc
RFC 089: drop the ARM64/Graviton Lambda decision
kenoir Jun 24, 2026
5816ebb
RFC 089: note bare-value reverse lookup ambiguity in open question 4
kenoir Jun 24, 2026
a2fe582
RFC 089: note FOLIO HRID vs UUID in the item-ingestion open question
kenoir Jun 24, 2026
3a359a9
RFC 089: tone pass on bold paragraph lead-ins
kenoir Jun 24, 2026
328f26d
RFC 089: settle open question 5 in favour of isAlias (no obsolete flag)
kenoir Jun 24, 2026
91ab560
RFC 089: close open question 6 (type enum scoped to three, extend on …
kenoir Jun 24, 2026
3eb1090
RFC 089: close open question 7 (type stays per-row, no top-level hoist)
kenoir Jun 24, 2026
fb59c37
RFC 089: settle open question 4 - defer the bare-value reverse lookup
kenoir Jun 24, 2026
d8f5b56
RFC 089: hoist a top-level type, and mark decided open questions
kenoir Jun 24, 2026
eb9e825
Merge branch 'main' into rk/identifiers-api-rfc
kenoir Jun 24, 2026
3d2024a
Regenerate RFC index table to reflect RFC 088 last-modified date
kenoir Jun 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions rfcs/089-identifiers-api/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# uv
.venv/

# Python
__pycache__/
*.py[cod]
*$py.class
.Python
*.so
1 change: 1 addition & 0 deletions rfcs/089-identifiers-api/.python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.12
445 changes: 445 additions & 0 deletions rfcs/089-identifiers-api/README.md

Large diffs are not rendered by default.

119 changes: 119 additions & 0 deletions rfcs/089-identifiers-api/openapi.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# Wellcome Collection Identifiers API

> Generated from [`openapi.yaml`](openapi.yaml) by `render_docs.py`. Do not edit by hand.
> Regenerate with `uv run python render_docs.py`.

**Version:** `1.0.0`

Resolves public catalogue (canonical) identifiers to their source identifiers and back. Mappings are one-to-many: a single canonical id can carry several source identifiers (an original plus "predecessor" aliases inherited during source-system migration).

## Servers

- `https://api.wellcomecollection.org`: Production edge

## Operations

### Tag: (untagged)

| Method | Path | Summary |
|---|---|---|
| `GET` | `/v1/identifiers/{canonicalId}` | Resolve a canonical id to its source identifiers |
| `GET` | `/v1/identifiers/by-source/{sourceSystem}/{value}` | Resolve a source identifier to its canonical id |

#### `GET /v1/identifiers/{canonicalId}`

_Resolve a canonical id to its source identifiers_

Returns the full set of source identifiers that share this canonical id, ordered by createdAt ascending so the original is always first. The set can grow during a source-system migration as aliases are inherited, so responses are cached with a bounded TTL and revalidated via ETag.

**Security:** `ApiKeyAuth`

**Parameters:**

| Name | In | Required | Type | Description |
|---|---|---|---|---|
| `canonicalId` | path | yes | string | 8-char public catalogue id. Alphabet a-z and 2-9, excluding o/i/l/1, first character a letter. Malformed ids are rejected at the gateway. |

**Responses:**

| Status | Body | Description |
|---|---|---|
| `200` | [`IdentifierSet`](#identifierset) | Canonical id is assigned and has at least one mapping. |
| `304` | n/a | |
| `400` | n/a | |
| `404` | n/a | |

#### `GET /v1/identifiers/by-source/{sourceSystem}/{value}`

_Resolve a source identifier to its canonical id_

Point lookup on the (OntologyType, SourceSystem, SourceId) primary key. By default returns only the canonical id. With include=siblings it also returns every source identifier sharing that canonical id (identical element shape to the forward lookup). The queried tuple is included in that set.

Freshness differs by variant: the bare lookup is immutable once minted and cached hard; include=siblings carries the mutable canonical->sources set and inherits the bounded-TTL + ETag treatment.

**Security:** `ApiKeyAuth`

**Parameters:**

| Name | In | Required | Type | Description |
|---|---|---|---|---|
| `sourceSystem` | path | yes | string | Source system identifier type. |
| `value` | path | yes | string | The identifier value within the source system. Must be a single, URL-encoded path segment. Source-id formats are heterogeneous, so this is not pattern-validated; unknown values resolve to 404 rather than 400. |
| `type` | query | no | string (enum: Work, Image, Item) | Ontology type. Part of the lookup key; defaults to Work. |
| `include` | query | no | string (enum: siblings) | Set to "siblings" to also return all source identifiers sharing the resolved canonical id. Omit for the canonical id alone. |

**Responses:**

| Status | Body | Description |
|---|---|---|
| `200` | [`CanonicalIdRef`](#canonicalidref) \| [`IdentifierSet`](#identifierset) | A mapping exists for the supplied tuple. |
| `304` | n/a | |
| `400` | n/a | |
| `404` | n/a | |

## Schemas

### SourceIdentifier

A single source identifier mapped to a canonical id.

**Required:** `type`, `sourceSystem`, `value`, `isAlias`, `createdAt`

| Property | Type | Required | Description |
|---|---|---|---|
| `type` | string (enum: Work, Image, Item) | yes | Ontology type for this row. Kept per-row because a canonical id can carry rows of differing types (cross-type predecessors are allowed). |
| `sourceSystem` | string | yes | |
| `value` | string | yes | |
| `isAlias` | boolean | yes | False for the original (earliest createdAt), true for inherited predecessor aliases. |
| `createdAt` | string (date-time) | yes | |

### IdentifierSet

A canonical id and the full set of source identifiers sharing it.

**Required:** `canonicalId`, `type`, `sourceIdentifiers`

| Property | Type | Required | Description |
|---|---|---|---|
| `canonicalId` | string | yes | |
| `type` | string (enum: Work, Image, Item) | yes | Ontology type of the canonical id, taken from the original row (the single isAlias=false row), so consumers need not scan the set. A convenience copy of that row's per-row type; with cross-type predecessors it reflects the original and may differ from a later alias. |
| `sourceIdentifiers` | array of [`SourceIdentifier`](#sourceidentifier) | yes | Ordered by createdAt ascending; original first. |

### CanonicalIdRef

Bare canonical id reference (default reverse-lookup response).

**Required:** `canonicalId`

| Property | Type | Required | Description |
|---|---|---|---|
| `canonicalId` | string | yes | |

### Error

**Required:** `error`, `message`

| Property | Type | Required | Description |
|---|---|---|---|
| `error` | string | yes | Stable machine-readable code. |
| `message` | string | yes | Human-readable detail. |
Loading
Loading