Organization identifiers
The OrganizationBase model currently hardcodes three U.S. federal identifier fields (ein, uei, duns). As CommonGrants expands to state, local, private, and international contexts, we need an extensible way to map multiple identifiers to a given organization, and ideally reuse the same pattern across other models (OpportunityBase, ApplicationBase, AwardBase).
How should the protocol represent the set of identifiers associated with an organization (or other resource), given that those identifiers come from different registries, follow different formats, and may change over time?
Decision
Section titled “Decision”The protocol adopts an identification system that supports both uniquely identifying a record within a given system and matching records across multiple systems. To do this, it replaces the hardcoded ein, uei, and duns fields on OrganizationBase with a generic identifiers collection. The same IdentifierCollection field will be reused across other core models, with each model extending it with its own base identifiers.
The collection looks like this:
{ "id": "01912a8b-7c3d-7890-abcd-ef1234567890", "name": "Example Nonprofit", "parent": { "id": "01912a8b-7c3d-7891-abcd-ef1234567891", "name": "Example Parent Foundation", "identifiers": { "ein": { "registry": { "code": "US-EIN", "url": "https://commongrants.org/registries/us-ein", "scope": "US", "kind": "government" }, "id": "98-7654321" } } }, "identifiers": { "systemId": { "registry": { "code": "grants.gov:org", "url": "https://commongrants.org/registries/grants-gov-org", "scope": "grants.gov", "kind": "platform" }, "id": "01912a8b-7c3d-7890-abcd-ef1234567890" }, "ein": { "registry": { "code": "US-EIN", "url": "https://commongrants.org/registries/us-ein", "scope": "US", "kind": "government", "schema": "https://commongrants.org/registries/us-ein/schema.json" }, "id": "12-3456789", "allIds": [ { "id": "12-3456789", "status": "active" }, { "id": "65-4321098", "status": "active" }, { "id": "11-1111111", "status": "archived" } ], "uri": "https://apps.irs.gov/app/eos/detailsPage?ein=123456789", "verifiedAt": "2026-03-15T00:00:00Z", "verifiedBy": "irs.gov" }, "uei": { "registry": { "code": "US-UEI", "url": "https://commongrants.org/registries/us-uei", "scope": "US", "kind": "government" }, "id": "AB0123456789" }, "otherIds": { "candidBridge": { "registry": { "code": "candid:bridge", "url": "https://commongrants.org/registries/candid-bridge", "scope": "candid", "kind": "commercial" }, "id": "1234567", "uri": "https://www.guidestar.org/profile/1234567" } } }}At a glance, the key decisions for this identification system are:
- Top-level ID: A pure UUID on each resource, with the hosting system’s own ID also surfaced inside the collection as
systemId. - Collection structure: A map keyed by short base-identifier aliases, with a catch-all
otherIdsmap for extensions. - Map key format: Model-defined camelCase aliases (
ein,uei,duns), not raw registry codes. - Identifier metadata structure: A nested hybrid shape. Registry-level facts (the same for every record in the registry) live in a nested
registryobject with requiredcodeandurl(linking to the catalog entry) plus optionalscope,kind, andschema. Instance-level facts (id,allIds,uri,verifiedAt,verifiedBy) stay at the top level. Richer registry metadata (issuer, human-readable name, lookup template, etc.) lives in the external CommonGrants registry catalog reachable throughregistry.url. - Multiple IDs per registry:
idfor the primary, plus an optionalallIdsarray of{ id, status }objects covering every known ID for this registry (the primary included), so consumers can do membership checks againstallIdsalone. - Parent organization:
parentcarriesid,name, and optionally an embeddedidentifierscollection. Parent identifiers do not leak into the child’s collection. - Registry code format: org-id.guide codes (
US-EIN,GB-CHC) for well-known public registries,namespace:type(grants.gov:org,candid:bridge) for everything else. - Registry governance: CommonGrants maintains a registry catalog that wraps org-id.guide where applicable and adds CommonGrants-specific metadata (schema URLs, kind, lookup templates). A documented promotion flow moves a registry from
otherIdsto a base identifier on the relevant model.
Consequences
Section titled “Consequences”- Positive consequences
- One reusable
Identifier/IdentifierCollectionpair across every core model, with model-specific keys on each extension. - Multiple IDs per registry are represented explicitly, with active, additional, and archived cases distinguishable.
- Consistent with existing patterns (
AddressCollection,customFields) and compatible with open data standards that use org-id.guide codes. - Nesting the
registryobject visually groups registry-level facts and points to the catalog entry viaregistry.url, so consumers can always find the source of truth. - The optional
registry.schemaURI lets new identifier types define their own validation without protocol changes. systemIdmakes each collection self-contained: a consumer can match across systems without inspecting the top-levelidseparately.
- One reusable
- Negative consequences
- Breaking change: existing
ein,uei, anddunsfields onOrganizationBasemust migrate. - Top-level
idis duplicated inidentifiers.systemId.id. - When
allIdsis present, the primaryidvalue is repeated as one of the entries (deliberate, so consumers only need to check one place for membership). - Registry-level fields inside
registrypartially duplicate the catalog entry atregistry.url, so adopters must keep them in sync. - Two extension mechanisms (
otherIdsfor new registries,allIdsfor multiple IDs in one registry) add complexity.
- Breaking change: existing
Identifier key hierarchy
Section titled “Identifier key hierarchy”Each IdentifierCollection has two tiers of keys:
- Base identifiers are defined by
@common-grants/coreat the root of each model’sIdentifierCollectionextension (e.g.ein,uei,dunsonOrgIdentifierCollection). Each one has an identifier-specific JSON schema that extends the genericIdentifierschema, plus a registered entry in the CommonGrants registry catalog. Every CommonGrants-compliant system is expected to recognize them for that model. otherIdsis a catch-all map for identifiers the protocol does not define. Adopters can publish any identifier here using any camelCase alias they choose (e.g.candidBridge,statePortalId). Entries need only conform to the genericIdentifierschema. They may optionally be listed in the CommonGrants registry catalog, but the key itself is not reserved or validated by the protocol.
When to use each:
- If the registry has a base identifier on the model, publish it under that key.
- If it doesn’t (yet), publish it under
otherIdswith a descriptive alias. - Consumers that want to consume new identifier types early can read from both places during the transition.
Identifier lifecycle
An identifier can move through two stages: getting listed in the CommonGrants registry catalog, and (eventually) being promoted to a base identifier on a model.
Stage 1: Register in the catalog (community-driven)
Cataloging an otherIds entry turns a one-off extension into a shared ID registry with a consistent alias, scope, kind, schema, and lookup template, so other systems can discover and reuse it. The key still lives under otherIds; cataloging just makes it easier for multiple systems to use this registry. Anyone can submit an entry by opening a PR that adds a file to the CommonGrants-hosted catalog, and a maintainer reviews it for the basics: is the registry coherent and well-documented, does it have a named issuer, and can values be validated.
Stage 2: Promote to a base identifier (maintainer-driven)
A registry that has seen enough adoption can later be promoted out of otherIds and into a base identifier slot on one or more models. Because base identifiers add reserved keys to a model’s IdentifierCollection extension, promotion ships as a minor release of @common-grants/core and becomes part of the protocol every compliant system is expected to recognize. That release requirement is what makes this stage maintainer-driven for now.
See Registry governance for the full rationale behind this two-stage approach.
Questions
Section titled “Questions”- Top-level ID format: Should the top-level
idbe a plain UUID, a prefixed UUID, or a registry-scoped composite string? - Collection structure: Should the identifier collection be a flat array or a keyed map?
- Map key format: If the collection is a map, should the keys be raw registry codes or short model-defined aliases?
- Identifier metadata structure: How much registry metadata belongs on each identifier instance, and how much should live in an external catalog?
- Multiple IDs per registry: How should the protocol represent multiple IDs that share the same registry, including archived versions?
- Parent organization representation: What should a parent organization reference carry?
- Registry code format: Should registry codes follow org-id.guide, a uniform namespaced format, or both?
- Registry governance: Who maintains the registry catalog, and how does a registry get promoted from
otherIdsto a base identifier?
Decision drivers
Section titled “Decision drivers”- Cross-system matching: Can identifiers match the same record across systems?
- Within-system location: Can an identifier locate and update a record in a given system?
- Standards interop: Does the approach work with org-id.guide and adjacent open data standards (OCDS, 360Giving, IATI)?
- Pattern alignment: Does it follow existing CommonGrants conventions (
AddressCollection,customFields, versioning)? - Format validation: Can well-known ID formats (EIN, UEI) be validated at the schema level?
- Extensibility: Can new identifier types be added without protocol changes?
- Multi-ID support: Can a record have multiple IDs in the same registry, and can archived IDs be distinguished from active ones?
- Reusability: Can the same types be used across
OrganizationBase,OpportunityBase,ApplicationBase, andAwardBase?
Options considered
Section titled “Options considered”- Top-level ID format: pure UUID, prefixed UUID, composite registry:id
- Collection structure: flat array, keyed map
- Map key format: raw registry code, camelCase alias
- Identifier metadata structure: flat single-string registry, hybrid, fully decomposed
- Multiple IDs per registry: array of
{ id, status }objects, flat array of strings - Parent representation: minimal reference, reference with identifiers, full summary
- Registry code format: uniform namespaced, hybrid (org-id.guide + namespaced)
- Registry governance: org-id.guide only, CommonGrants-managed only, hybrid with promotion flow
Schema definitions
Section titled “Schema definitions”Identifier
Identifier: type: object properties: registry: type: object description: | Registry-level facts that are the same for every record in this registry. A subset of the corresponding catalog entry; consumers can fetch the full entry via `registry.url`. properties: code: type: string description: Registry code (e.g. "US-EIN", "grants.gov:org") url: type: string format: uri description: Link to the catalog entry for this registry scope: type: string description: Jurisdiction or namespace the registry applies within (e.g. "US", "GB", "grants.gov") kind: type: string enum: [government, commercial, platform, system] description: | Classification of the registry: - government: issued by a government authority (EIN, UEI) - commercial: issued by a commercial data provider (DUNS, Candid Bridge) - platform: assigned by a CommonGrants-compliant system - system: assigned by a non-CommonGrants system schema: type: string format: uri description: JSON schema for validating the identifier format required: [code, url] id: type: string description: Primary current identifier string allIds: type: array description: | Every known ID for this record in this registry, including the primary `id` and any additional or archived IDs (e.g. fiscal sponsor EINs, retired UEIs). When present, the primary `id` MUST also appear here so consumers can do membership checks (e.g. `recordId in allIds.map(v => v.id)`) without also consulting `id` separately. items: type: object properties: id: type: string description: The identifier string status: type: string enum: [active, archived] description: Whether the identifier is currently valid or retired required: [id, status] uri: type: string format: uri description: Link to this record in the registry's lookup service verifiedAt: type: string format: date-time description: When this identifier was last verified verifiedBy: type: string description: | Reference to the system, service, or process that performed the verification (e.g. "irs.gov", "candid.org", "system:manual-review"). Be mindful of PII such as a human name or email address. The exact format and vocabulary for this field may be tightened during implementation. required: - registry - idIdentifierCollection
IdentifierCollection: type: object properties: systemId: $ref: "#/components/schemas/Identifier" description: The hosting system's own ID for this record otherIds: type: object additionalProperties: $ref: "#/components/schemas/Identifier" description: Extension point for non-standard registriesOrgIdentifierCollection (model-specific extension)
OrgIdentifierCollection: allOf: - $ref: "#/components/schemas/IdentifierCollection" - type: object properties: ein: $ref: "#/components/schemas/Identifier" uei: $ref: "#/components/schemas/Identifier" duns: $ref: "#/components/schemas/Identifier"Cross-model reuse
Section titled “Cross-model reuse”The same shape extends to other models with model-specific keys:
{ "id": "01912a8b-7c3d-7892-abcd-ef1234567892", "title": "Community Health Initiative Grant", "identifiers": { "systemId": { "registry": { "code": "grants.gov:opp", "url": "https://commongrants.org/registries/grants-gov-opp", "scope": "grants.gov", "kind": "platform" }, "id": "01912a8b-7c3d-7892-abcd-ef1234567892" }, "opportunityNumber": { "registry": { "code": "grants.gov:opp", "url": "https://commongrants.org/registries/grants-gov-opp", "scope": "grants.gov", "kind": "platform" }, "id": "HHS-2026-ACF-OCC-YD-0001" }, "cfda": { "registry": { "code": "US-CFDA", "url": "https://commongrants.org/registries/us-cfda", "scope": "US", "kind": "government" }, "id": "93.575" } }}{ "id": "01912a8b-7c3d-7893-abcd-ef1234567893", "identifiers": { "systemId": { "registry": { "code": "fluxx:app", "url": "https://commongrants.org/registries/fluxx-app", "scope": "fluxx", "kind": "system" }, "id": "01912a8b-7c3d-7893-abcd-ef1234567893" }, "otherIds": { "grantsGovTracking": { "registry": { "code": "grants.gov:tracking", "url": "https://commongrants.org/registries/grants-gov-tracking", "scope": "grants.gov", "kind": "platform" }, "id": "GRANT12345678" } } }}{ "id": "01912a8b-7c3d-7894-abcd-ef1234567894", "identifiers": { "systemId": { "registry": { "code": "grants.gov:award", "url": "https://commongrants.org/registries/grants-gov-award", "scope": "grants.gov", "kind": "platform" }, "id": "01912a8b-7c3d-7894-abcd-ef1234567894" }, "fain": { "registry": { "code": "US-FAIN", "url": "https://commongrants.org/registries/us-fain", "scope": "US", "kind": "government" }, "id": "ABC12345" } }}Cross-system lookup route
Section titled “Cross-system lookup route”List routes accept two new query parameters, registry and id, that together let consumers locate a record by any of its registered identifiers without knowing the local UUID:
GET /organizations?registry=US-EIN&id=12-3456789GET /opportunities?registry=US-CFDA&id=93.575GET /applications?registry=fluxx:app&id=app-98765registry may be passed on its own as a filter (e.g. ?registry=US-CFDA returns every record that has a CFDA entry). id on its own is meaningless without a registry to scope it to and SHOULD be rejected with a 400.
Evaluation
Section titled “Evaluation”Top-level ID format
Section titled “Top-level ID format”Should the top-level id be a plain UUID, a prefixed UUID, or a registry-scoped composite string?
Side-by-side comparison
Section titled “Side-by-side comparison”- ✅ Criterion met
- ❌ Criterion not met
- 🟡 Partially met or unsure
| Criteria | Pure UUID (rec.) | Prefixed UUID | Composite registry:id |
|---|---|---|---|
Consistent with other CommonGrants id fields | ✅ | ❌ | ❌ |
| Compatible with native UUID database columns | ✅ | ❌ | ❌ |
| RFC-standard UUID (stdlib parsers, validators) | ✅ | ❌ | ❌ |
| Stable if external identifiers change | ✅ | ✅ | ❌ |
| Standard REST path parameter | ✅ | ✅ | 🟡 |
| Self-describing in logs | ❌ | ✅ | ✅ |
Option 1: Pure UUID (recommended)
Section titled “Option 1: Pure UUID (recommended)”Example
# Direct lookup by the system's UUID for this organizationGET /organizations/01912a8b-7c3d-7890-abcd-ef1234567890
# Cross-system lookup by registry-scoped identifier (here: the org's EIN)GET /organizations?registry=US-EIN&id=12-3456789- Pros
- Consistent with how every other CommonGrants model represents
id(Opportunity, Application, Award, etc.). - Aligns with common database and platform-specific ID conventions (UUID columns, existing API keys, platform primary keys).
- Works with standard REST patterns:
GET /organizations/{uuid}just works. - UUIDs remain stable even if external identifiers change.
- Full ecosystem support: standard validators, most languages’ stdlib, OpenAPI
format: uuid. - Cross-system lookups are handled by separate
registryandidquery parameters on list routes, so the top-leveliddoesn’t need to encode any cross-system context. - Implementers can use UUIDv7 for time-ordered, index-friendly IDs without the protocol specifying a version.
- Consistent with how every other CommonGrants model represents
- Cons
- Top-level
idduplicatesidentifiers.systemId.id. - UUIDs carry no information about the record on their own, so logs and error messages need an extra lookup for context.
- Top-level
Option 2: Prefixed UUID (also known as TypeID)
Section titled “Option 2: Prefixed UUID (also known as TypeID)”For organizations, the prefix would be org_. Adopting this would be a protocol-wide decision (Opportunity, Competition, Form, Application, Award, etc. all have UUIDs today), so it’s flagged here but deferred to a future ADR that can apply the convention consistently across all models.
Example
GET /organizations/org_01912a8b-7c3d-7890-abcd-ef1234567890- Pros
- Resource type is visible at a glance (
org_...,opp_...,app_...). - Popular in API design (Stripe and similar) and easy to grep for.
- TypeID has a growing ecosystem of language bindings and a published spec.
- Resource type is visible at a glance (
- Cons
- Not an RFC-standard UUID string: loses native database UUID column support, most UUID libraries, and standard validators.
- Requires every client and SDK to parse the prefix out before using the underlying UUID.
- Needs to be applied consistently across every model with an
idfield (Opportunity, Competition, Form, Application, Award, etc.) and would also require updating each model’sidvalidation to accept the prefixed format. Out of scope for this ADR.
Option 3: Composite registry:id string
Section titled “Option 3: Composite registry:id string”Example
GET /organizations/US-EIN:12-3456789- Pros
- Readable:
US-EIN:12-3456789makes the registry and value visible in logs and URLs. - One string carries both registry and value, so no separate field is needed.
- Readable:
- Cons
- Forces systems to pick one registry as canonical, which is arbitrary for orgs with multiple IDs.
- Colons in URL paths complicate routing and URL parsing in several web frameworks.
- If the organization corrects or replaces the underlying ID (e.g. EIN reassignment), every stored reference breaks.
Collection structure
Section titled “Collection structure”Should the identifier collection be a flat array or a keyed map?
Side-by-side comparison
Section titled “Side-by-side comparison”| Criteria | Map (rec.) | Flat array |
|---|---|---|
| Keyed access without filtering | ✅ | ❌ |
| Per-registry JSON schemas | ✅ | ❌ |
| Consistent with existing CG collections | ✅ | ❌ |
| Clear primary vs. alternate IDs per registry | ✅ | 🟡 |
| Cross-system matching | ✅ | ✅ |
| Mirrors org-id.guide wire shape | 🟡 | ✅ |
Option 1: Map (recommended)
Section titled “Option 1: Map (recommended)”Example
{ "identifiers": { "ein": { "registry": { "code": "US-EIN", "url": "https://commongrants.org/registries/us-ein" }, "id": "12-3456789", "allIds": [ { "id": "12-3456789", "status": "active" }, { "id": "98-7654321", "status": "active" } ] }, "uei": { "registry": { "code": "US-UEI", "url": "https://commongrants.org/registries/us-uei" }, "id": "AB0123456789" } }}const ein = org.identifiers.ein;ein.id; // "12-3456789" (primary)ein.allIds.map((v) => v.id); // ["12-3456789", "98-7654321"] (every known ID)- Pros
- Consistent with existing CommonGrants map collections (
AddressCollection,customFields). - Direct keyed access with per-key JSON schemas, so validators can branch on the registry.
- Clear separation of primary vs. alternate values per registry.
otherIdsgives a natural extension point for non-standard registries.
- Consistent with existing CommonGrants map collections (
- Cons
- Differs structurally from org-id.guide, even though codes are shared.
otherIdsnesting adds one level of indirection for extensions.
Option 2: Flat array
Section titled “Option 2: Flat array”Example
{ "identifiers": [ { "scheme": "US-EIN", "id": "12-3456789" }, { "scheme": "US-EIN", "id": "98-7654321" }, { "scheme": "US-UEI", "id": "AB0123456789" } ]}const ein = org.identifiers.find((i) => i.scheme === "US-EIN");- Pros
- Mirrors org-id.guide’s
scheme+idmodel directly. - Well-established in open data standards (OCDS, 360Giving, IATI).
- Mirrors org-id.guide’s
- Cons
- Accessing a specific identifier requires filtering by scheme.
- Per-registry validation is hard in a mixed array because JSON schema can’t branch on
schemecleanly. - Multiple IDs per registry appear as duplicate entries with no clear primary.
Map key format
Section titled “Map key format”If the collection is a map, should the keys be raw registry codes or short model-defined aliases?
Side-by-side comparison
Section titled “Side-by-side comparison”| Criteria | camelCase alias (rec.) | Registry code as key |
|---|---|---|
| Dot access in JS, Python, Go, Ruby | ✅ | ❌ |
| Consistent with existing CG map key convention | ✅ | ❌ |
| Stable if registry codes are renamed | ✅ | ❌ |
No duplication with inline registry field | ✅ | ❌ |
| Works cleanly in JSON Pointer / JSONPath | ✅ | 🟡 |
| Self-describing without catalog lookup | 🟡 | ✅ |
Option 1: camelCase alias (recommended)
Section titled “Option 1: camelCase alias (recommended)”Example
{ "identifiers": { "ein": { "registry": { "code": "US-EIN", "url": "https://commongrants.org/registries/us-ein" }, "id": "12-3456789" }, "uei": { "registry": { "code": "US-UEI", "url": "https://commongrants.org/registries/us-uei" }, "id": "AB0123456789" }, "otherIds": { "candidBridge": { "registry": { "code": "candid:bridge", "url": "https://commongrants.org/registries/candid-bridge" }, "id": "1234567" } } }}const ein = org.identifiers.ein;const candid = org.identifiers.otherIds.candidBridge;- Pros
- Dot access works in every major language, no bracket notation required.
- Matches the existing CommonGrants map convention (
customFields,addresses,emails). - Aliases stay stable even if the underlying registry code changes.
- Keys stay short and clean in JSON Pointer paths, logs, and query parameters.
- No duplication with the
registry.codefield inside each entry.
- Cons
- Consumers need a small alias-to-registry-code mapping, maintained in the CommonGrants registry catalog.
- Aliases can shadow each other across scopes (e.g. an EIN-like ID in another jurisdiction), so the catalog must prevent collisions.
Option 2: Registry code as key
Section titled “Option 2: Registry code as key”Example
{ "identifiers": { "US-EIN": { "registry": { "code": "US-EIN", "url": "https://commongrants.org/registries/us-ein" }, "id": "12-3456789" }, "US-UEI": { "registry": { "code": "US-UEI", "url": "https://commongrants.org/registries/us-uei" }, "id": "AB0123456789" } }}const ein = org.identifiers["US-EIN"];- Pros
- Key tells you immediately which registry the entry refers to.
- No alias mapping needed: consumers can use org-id.guide codes directly.
- Cons
- Uppercase and hyphenated keys force bracket notation in most languages (JS, Python, Go).
- Duplicates the registry code into both the key and the body (
registry.code). - Couples the key to the registry’s naming: if org-id.guide renames a code, every consumer has to adapt.
- Inconsistent with the rest of the CommonGrants spec, where map keys are camelCase.
Identifier metadata structure
Section titled “Identifier metadata structure”How much registry metadata belongs on each identifier instance, and how much should live in an external catalog?
All three options below share the same principle: registry-level facts (the same for every record in this registry) and instance-level facts (specific to this record’s entry) are conceptually distinct. They differ in how the registry-level facts are expressed.
Side-by-side comparison
Section titled “Side-by-side comparison”| Criteria | Nested hybrid (rec.) | Flat | Fully decomposed |
|---|---|---|---|
| Explicit pointer to the catalog entry | ✅ | ✅ | ✅ |
| Registry vs. instance fields visually grouped | ✅ | ❌ | ✅ |
| Inline filtering by scope or kind | ✅ | ❌ | ✅ |
| Per-registry schema validation | ✅ | ❌ | ✅ |
| Avoids duplicating catalog metadata per record | ✅ | ✅ | ❌ |
| Payload brevity | 🟡 | ✅ | ❌ |
| Fully self-contained without a catalog | 🟡 | ❌ | ✅ |
| Few required fields | ✅ | ✅ | ❌ |
Option 1: Nested hybrid (recommended)
Section titled “Option 1: Nested hybrid (recommended)”The registry object holds a curated subset of the catalog entry: the fields that are useful in payload (filtering, validation, identification) plus a url linking back to the full catalog entry. Required fields are kept minimal (code and url); the rest are optional. Per-instance fields stay at the top level.
Example
{ "ein": { "registry": { "code": "US-EIN", "url": "https://commongrants.org/registries/us-ein", "scope": "US", "kind": "government", "schema": "https://commongrants.org/registries/us-ein/schema.json" }, "id": "12-3456789", "uri": "https://apps.irs.gov/app/eos/detailsPage?ein=123456789", "verifiedAt": "2026-03-15T00:00:00Z" }}The CommonGrants registry catalog at registry.url carries the rest:
code: US-EINname: Employer Identification Numberscope: USkind: governmentissuer: US Internal Revenue Serviceschema: https://commongrants.org/registries/us-ein/schema.jsonlookupUriTemplate: https://apps.irs.gov/app/eos/detailsPage?ein={id}source: https://org-id.guide/list/US-EIN- Pros
registry.urlgives consumers an explicit pointer to the catalog entry, removing ambiguity about how inline and catalog fields relate.- Nesting visually groups registry-level vs. instance-level facts, mirroring the catalog shape.
scopeandkindare available inline for filtering and UI decisions (e.g. “show all government-issued IDs”) without a catalog fetch.schemasupports per-registry validation in JSON Schema.- Required surface stays minimal: only
registry.code,registry.url, andidare required; everything else is optional. - Verbose metadata (full name, issuer, lookup template) stays in the catalog so payloads don’t bloat.
- Cons
- One extra level of access (
identifier.registry.codevs. a flatidentifier.registry). - Inline registry fields can drift from the catalog if not kept in sync.
scopeandkindare technically derivable fromregistry.codevia the catalog, so they are redundant.
- One extra level of access (
Option 2: Flat with registryUrl
Section titled “Option 2: Flat with registryUrl”Example
{ "ein": { "registry": "US-EIN", "registryUrl": "https://commongrants.org/registries/us-ein", "id": "12-3456789" }}- Pros
- Minimal shape: just
registry(code),registryUrl, andid. - Direct keyed access without extra nesting.
registryUrlstill gives a clear pointer to the catalog.
- Minimal shape: just
- Cons
- No way to filter by scope or kind without a catalog lookup.
- No in-payload hint about whether the registry is authoritative, commercial, or platform-specific.
- No format validation at the schema level without consulting the catalog.
- Registry-level vs. instance-level facts are not visually grouped.
Option 3: Fully decomposed
Section titled “Option 3: Fully decomposed”Example
{ "ein": { "registry": { "code": "US-EIN", "url": "https://commongrants.org/registries/us-ein", "name": "Employer Identification Number", "scope": "US", "kind": "government", "issuer": "US Internal Revenue Service", "schema": "https://commongrants.org/registries/us-ein/schema.json", "lookupUriTemplate": "https://apps.irs.gov/app/eos/detailsPage?ein={id}" }, "id": "12-3456789" }}- Pros
- No catalog lookup needed for any consumer decision.
- Each identifier is self-documenting end to end.
- Cons
- Payloads bloat quickly, especially for records with many identifiers.
- Registry metadata is duplicated across every record and drifts over time.
- Still needs a canonical catalog somewhere to resolve conflicts when two records disagree.
Multiple IDs per registry
Section titled “Multiple IDs per registry”How should the protocol represent multiple IDs that share the same registry, including archived versions?
In both options below, the primary ID stays at the top level as id. When allIds is present, it includes every known ID for this registry (the primary value included), so consumers can run a single membership check against allIds without also inspecting id separately. The difference between the two options is whether each entry is a bare string or a { id, status } object.
Side-by-side comparison
Section titled “Side-by-side comparison”| Criteria | Array of { id, status } (rec.) | Flat array of strings |
|---|---|---|
| Distinguishes active vs. archived IDs | ✅ | ❌ |
| Unambiguous primary value | ✅ | ✅ |
| Zero overhead for the common single-value case | ✅ | ✅ |
| Extensible without breaking changes | ✅ | 🟡 |
| Simple to populate and read | 🟡 | ✅ |
Native contains() / in membership without projecting | ❌ | ✅ |
Option 1: Array of { id, status } objects (recommended)
Section titled “Option 1: Array of { id, status } objects (recommended)”Example
{ "ein": { "registry": { "code": "US-EIN", "url": "https://commongrants.org/registries/us-ein" }, "id": "12-3456789", "allIds": [ { "id": "12-3456789", "status": "active" }, { "id": "98-7654321", "status": "active" }, { "id": "11-1111111", "status": "archived" } ] }}- Pros
idis the single clear primary, butallIdscovers every known value (including the primary) so a single membership check is sufficient:recordId in ein.allIds.map(v => v.id).status: "active"cleanly covers fiscal sponsor, post-merger, and multi-subsidiary cases.status: "archived"covers UEI rebrands, EIN reassignments, and similar history.- Consumers that don’t care about alternates can ignore
allIdsentirely and just useid. - Extensible: future optional fields (
validFrom,note, etc.) can be added per entry without breaking consumers.
- Cons
- Small object wrapper per entry rather than a bare string.
- Consumers that want to filter archived entries must check the
statusfield. - Membership checks require projecting first (
ein.allIds.map(v => v.id).includes(x)) instead of a nativein/contains()check. - The primary
idvalue is duplicated acrossidand the correspondingallIdsentry.
Option 2: Flat array of strings
Section titled “Option 2: Flat array of strings”Example
{ "ein": { "registry": { "code": "US-EIN", "url": "https://commongrants.org/registries/us-ein" }, "id": "12-3456789", "allIds": ["12-3456789", "98-7654321", "11-1111111"] }}- Pros
- Simple array of strings, no object wrapper.
- Native
contains()/inmembership checks againstallIdswork directly:"98-7654321" in ein.allIds.
- Cons
- Can’t tell which IDs are still valid and which are archived.
- Can’t tell the difference between a fiscal-sponsor case (two active EINs) and a rename history.
- Adding per-entry metadata later (status, timestamps) would be a breaking change.
- The primary
idvalue is duplicated acrossidand theallIdsarray.
Parent organization representation
Section titled “Parent organization representation”What should a parent organization reference carry?
This sub-decision applies only to OrganizationBase. Other models (OpportunityBase, ApplicationBase, AwardBase) do not currently carry a parent concept.
Side-by-side comparison
Section titled “Side-by-side comparison”| Criteria | With identifiers (rec.) | Minimal | Full summary |
|---|---|---|---|
| One-hop cross-system matching on the parent | ✅ | ❌ | ✅ |
No duplication of OrganizationBase fields | ✅ | ✅ | ❌ |
| Bounded scope (no risk of grandparent bloat) | ✅ | ✅ | ❌ |
| Low drift risk vs. canonical parent record | 🟡 | ✅ | ❌ |
| Payload brevity | 🟡 | ✅ | ❌ |
| Renderable without a follow-up fetch | 🟡 | ❌ | ✅ |
Option 1: Reference with identifiers (recommended)
Section titled “Option 1: Reference with identifiers (recommended)”Example
{ "parent": { "id": "01912a8b-7c3d-7891-abcd-ef1234567891", "name": "Example Parent Foundation", "identifiers": { "ein": { "registry": { "code": "US-EIN", "url": "https://commongrants.org/registries/us-ein", "scope": "US", "kind": "government" }, "id": "98-7654321" } } }}- Pros
- Enables one-hop cross-system matching on the parent (e.g. “find all children of this UEI”).
- Still lightweight: only identifiers are embedded, not addresses or mission.
- Parent IDs never leak into the child’s own
identifiers, so the child collection stays self-referential.
- Cons
- Parent identifier data can drift from the authoritative parent record if not kept in sync.
- Slightly more complex to serialize than a bare reference.
Option 2: Minimal reference
Section titled “Option 2: Minimal reference”Example
{ "parent": { "id": "01912a8b-7c3d-7891-abcd-ef1234567891", "name": "Example Parent Foundation" }}- Pros
- Smallest possible shape.
- No risk of the parent reference drifting from the parent’s canonical record.
- Cons
- Can’t match the parent across systems without another fetch.
- Display surfaces (e.g. “parent org: UEI AB123…”) require extra round trips.
Option 3: Full summary
Section titled “Option 3: Full summary”Example
{ "parent": { "id": "01912a8b-7c3d-7891-abcd-ef1234567891", "name": "Example Parent Foundation", "orgType": "private_foundation", "identifiers": { "ein": { "registry": { "code": "US-EIN", "url": "https://commongrants.org/registries/us-ein" }, "id": "98-7654321" } }, "addresses": { "primary": { "line1": "...", "city": "...", "state": "...", "postalCode": "..." } } }}- Pros
- Enough data to render a parent card with no follow-up fetch.
- Cons
- Duplicates large portions of
OrganizationBase. - High risk of drift between parent reference and canonical parent record.
- Unclear where the summary should stop: what about the grandparent?
- Duplicates large portions of
Registry code format
Section titled “Registry code format”Should registry codes follow org-id.guide, a uniform namespaced format, or both?
Side-by-side comparison
Section titled “Side-by-side comparison”| Criteria | org-id.guide + namespaced (rec.) | Uniform namespace:type |
|---|---|---|
| Zero-translation interop with open data standards | ✅ | ❌ |
| Recognizable to OCDS, 360Giving, IATI users | ✅ | ❌ |
| Visually distinguishes standard vs. system registries | ✅ | ❌ |
| Covers non-standard system IDs | ✅ | ✅ |
| Internal format consistency | 🟡 | ✅ |
Option 1: org-id.guide + namespaced (recommended)
Section titled “Option 1: org-id.guide + namespaced (recommended)”Example
{ "ein": { "registry": { "code": "US-EIN", "url": "https://commongrants.org/registries/us-ein" }, "id": "12-3456789" }, "otherIds": { "fluxxOrg": { "registry": { "code": "fluxx:org", "url": "https://commongrants.org/registries/fluxx-org" }, "id": "FLUXX-12345" } }}- Pros
- Uses org-id.guide codes as-is, so existing open data systems interoperate without translation.
- Visually distinguishes standardized registries (
US-EIN) from system-specific ones (fluxx:org). - Anyone familiar with OCDS, 360Giving, or IATI recognizes the codes at a glance.
- Cons
- Two formats coexist: uppercase-hyphenated and lowercase-colon.
- Camel-case alias keys help, but the registry field itself still requires bracket notation when used as an object key elsewhere.
Option 2: Uniform namespace:type
Section titled “Option 2: Uniform namespace:type”Example
{ "ein": { "registry": { "code": "us:ein", "url": "https://commongrants.org/registries/us-ein" }, "id": "12-3456789" }, "uei": { "registry": { "code": "us:uei", "url": "https://commongrants.org/registries/us-uei" }, "id": "AB0123456789" }}- Pros
- One format for everything.
- No need to decide when to use hyphens vs. colons.
- Cons
- Requires a mapping table to interoperate with org-id.guide.
- Not recognizable to anyone already using OCDS, 360Giving, or IATI.
Registry governance
Section titled “Registry governance”Who maintains the registry catalog, and how does a registry get promoted from otherIds to a base identifier?
Side-by-side comparison
Section titled “Side-by-side comparison”| Criteria | Hybrid (rec.) | org-id.guide only | CG-managed only |
|---|---|---|---|
| Leverages existing open data taxonomy | ✅ | ✅ | ❌ |
CG-specific metadata (schema, kind, alias) | ✅ | ❌ | ✅ |
Covers non-standard system IDs (fluxx:org, etc.) | ✅ | ❌ | ✅ |
| Promotion path independent of upstream release cycle | ✅ | 🟡 | ✅ |
Clear otherIds → base identifier flow | ✅ | 🟡 | ✅ |
| Low maintenance burden | 🟡 | ✅ | ❌ |
Option 1: Hybrid with promotion flow (recommended)
Section titled “Option 1: Hybrid with promotion flow (recommended)”Shape of the CommonGrants registry catalog
Each catalog entry wraps an org-id.guide entry (when one exists) and adds CG-specific fields:
code: US-EINalias: einname: Employer Identification Numberscope: USkind: governmentissuer: US Internal Revenue Serviceschema: https://commongrants.org/registries/us-ein/schema.jsonlookupUriTemplate: https://apps.irs.gov/app/eos/detailsPage?ein={id}source: https://org-id.guide/list/US-EINmodels: [Organization]Promotion flow
Any identifier that starts in otherIds can become a base identifier through the promotion flow documented in Identifier key hierarchy.
- Pros
- Reuses org-id.guide codes where they exist, avoiding duplicated taxonomy work.
- Adds CG-specific metadata (schema, kind, alias) in one place.
- Clear, documented path from ad-hoc use to first-class protocol support.
- Parallels the way custom fields are promoted today.
- Cons
- Two sources of truth (org-id.guide and the CG catalog) that must be kept in sync.
- Promotion flow adds process overhead that some adopters may route around.
Option 2: org-id.guide only
Section titled “Option 2: org-id.guide only”- Pros
- No catalog maintenance: codes, scopes, and lookups live in org-id.guide.
- Strong ecosystem alignment.
- Cons
- No standard place to attach JSON schemas for per-registry validation.
- No concept of
kind: platformorkind: systemfor non-public registries. - Getting a new registry added to org-id.guide is external to our release cycle.
Option 3: CG-managed catalog only
Section titled “Option 3: CG-managed catalog only”- Pros
- Complete control over codes, scopes, schemas, and lookup templates.
- Can carry CG-specific fields (
kind,schema).
- Cons
- Duplicates org-id.guide entries, which must be kept in sync manually.
- CG becomes the single point of failure for registry definitions.