# Stable identifiers

A page id looks like `D0033` — one or two capitals then four digits — and is the stable handle for that page. Object identifiers elsewhere are a short prefix and an opaque remainder: `ev_`, `fu_`, `ws_`. Treat every one of them as a string to compare, never as a structure to read meaning out of. One identifier deliberately changes: the delivery attempt.

- **Status:** Available
- **Audience:** developer
- **Last verified:** 2026-09-10
- **Canonical:** https://connectbyjbrh.com/developers/entity-ids/

## Page identifiers

Every published page has an id matching `^[A-Z]{1,2}\d{4}$`. It appears in the manifest, in every search result and in every fetched record, and it is the argument the MCP tool `fetch_public_doc` and the `ref` parameter both accept.

The id is stable across rewording, retitling and even a change of URL. That is the whole reason it exists: a URL is a location and can be improved, while a citation stored in somebody's database six months ago needs to keep resolving. If you are keeping a reference to a page, keep the id and treat the URL as display.

```http
GET /api/public/docs/page?ref=D0033 HTTP/1.1
GET /api/public/docs/page?ref=/developers/entity-ids/ HTTP/1.1
GET /api/public/docs/page?ref=https://connectbyjbrh.com/developers/entity-ids/index.md HTTP/1.1

# all three resolve to the same page
```

The reference parameter is forgiving on purpose. A full URL is reduced to a path, a query string and fragment are dropped, a trailing `index.md` is removed, and a missing leading or trailing slash is added. An id is uppercased before lookup, so `d0033` works — but the canonical form in every response is uppercase, and that is the form to store.

> **Careful** Do not derive a page's `kind` from the letter in its id. `kind` is a field in the record; read it. A rule that infers meaning from a prefix is a rule that breaks silently the first time a prefix is reused.

## Prefixed object identifiers

| Form | Names | Stability |
|---|---|---|
| `ev_…` | One event in the published webhook contract | Stable across retries — this is what you de-duplicate on |
| `fu_…` | A follow-up, inside an event's `data` | Stable for that follow-up |
| `ws_…` | A workspace | Stable. Always your own in an event you receive |
| `X-Connect-Delivery` | One delivery attempt | **Changes on every retry.** Not an identity for the event |
| `key_id` | An integration key record | Stable. Safe to log; the key itself is not |

The prefix tells you what kind of thing it is and nothing else. The remainder is opaque: no documented length, no documented alphabet, no embedded timestamp, no ordering guarantee. Sorting a set of `ev_` identifiers does not order the events; `occurredAt` does that.

Store them as text with room to spare. A column sized to today's observed length is the most common way an opaque identifier stops being opaque — it becomes a constraint nobody agreed to.

## URLs as identifiers

A canonical page URL ends in a slash. `/developers/entity-ids` and `/developers/entity-ids/` would otherwise be two URLs for one page, so the server redirects the first to the second — and deliberately leaves anything with a file extension alone, which is how `index.md` is served rather than bounced.

That makes the trailing slash part of the identity when you are comparing URLs. Normalise before you compare, or compare on `id` instead, which is why the id is there.

Absolute and relative forms both appear in one record: `url` and `markdown` are absolute, `related` entries are site-relative paths. Resolve `related` against the site root once, in one place in your client.

## Five rules that survive contact with production

1. Compare identifiers with exact string equality. No trimming that changes case, no normalisation beyond what a response gave you.
2. Never parse an identifier for a date, a sequence number, a workspace or a type. Read the field that carries the fact.
3. De-duplicate on the event `id`, never on the delivery header — one is designed to repeat, the other is designed to differ.
4. Keep the page `id` as your durable reference and the URL as the thing you show a person.
5. Treat a key's plaintext as a secret and its `key_id` as the identifier. They are not two names for one thing.

## Questions

### If a page's URL changes, does its id change?

No — that separation is the reason both exist. The id follows the page; the URL describes where it currently lives.

### Are page ids sequential in publication order?

Do not depend on it. They are allocated by the plan that produced the corpus, and a numeric neighbour is not a chronological one. If you want order, sort on `url` or on `updated`.

### Can two pages share an id?

No. The build checks the identifier format and uniqueness across the whole corpus before anything is published, so an id resolves to exactly one page or to nothing.

## Related

- [The public data model](https://connectbyjbrh.com/developers/data-model-public/)
- [The machine-readable documentation](https://connectbyjbrh.com/developers/machine-manifests/)
- [Time in the API](https://connectbyjbrh.com/developers/time-and-timezones/)
- [Outbound webhooks](https://connectbyjbrh.com/developers/webhooks-outbound/)
- [Searching the documentation programmatically](https://connectbyjbrh.com/developers/docs-search-api/)

## What this page is based on

- `backend/app/public_docs.py` — `_normalise_path`, `by_id`, `canonical_redirect`
- `docs-source/schema.py` — the identifier pattern
- `webapp/developers/asyncapi.yaml` — event, follow-up and delivery identifiers
- `backend/app/integration_auth.py` — `key_id` against the plaintext
