Skip to content

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?

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:

  1. Top-level ID: A pure UUID on each resource, with the hosting system’s own ID also surfaced inside the collection as systemId.
  2. Collection structure: A map keyed by short base-identifier aliases, with a catch-all otherIds map for extensions.
  3. Map key format: Model-defined camelCase aliases (ein, uei, duns), not raw registry codes.
  4. Identifier metadata structure: A nested hybrid shape. Registry-level facts (the same for every record in the registry) live in a nested registry object with required code and url (linking to the catalog entry) plus optional scope, kind, and schema. 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 through registry.url.
  5. Multiple IDs per registry: id for the primary, plus an optional allIds array of { id, status } objects covering every known ID for this registry (the primary included), so consumers can do membership checks against allIds alone.
  6. Parent organization: parent carries id, name, and optionally an embedded identifiers collection. Parent identifiers do not leak into the child’s collection.
  7. 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.
  8. 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 otherIds to a base identifier on the relevant model.
  • Positive consequences
    • One reusable Identifier / IdentifierCollection pair 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 registry object visually groups registry-level facts and points to the catalog entry via registry.url, so consumers can always find the source of truth.
    • The optional registry.schema URI lets new identifier types define their own validation without protocol changes.
    • systemId makes each collection self-contained: a consumer can match across systems without inspecting the top-level id separately.
  • Negative consequences
    • Breaking change: existing ein, uei, and duns fields on OrganizationBase must migrate.
    • Top-level id is duplicated in identifiers.systemId.id.
    • When allIds is present, the primary id value is repeated as one of the entries (deliberate, so consumers only need to check one place for membership).
    • Registry-level fields inside registry partially duplicate the catalog entry at registry.url, so adopters must keep them in sync.
    • Two extension mechanisms (otherIds for new registries, allIds for multiple IDs in one registry) add complexity.

Each IdentifierCollection has two tiers of keys:

  • Base identifiers are defined by @common-grants/core at the root of each model’s IdentifierCollection extension (e.g. ein, uei, duns on OrgIdentifierCollection). Each one has an identifier-specific JSON schema that extends the generic Identifier schema, plus a registered entry in the CommonGrants registry catalog. Every CommonGrants-compliant system is expected to recognize them for that model.
  • otherIds is 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 generic Identifier schema. 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 otherIds with 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.

  • Top-level ID format: Should the top-level id be 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 otherIds to a base identifier?
  • 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, and AwardBase?
  • 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
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
- id
IdentifierCollection
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 registries
OrgIdentifierCollection (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"

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"
}
}
}

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-3456789
GET /opportunities?registry=US-CFDA&id=93.575
GET /applications?registry=fluxx:app&id=app-98765

registry 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.

Should the top-level id be a plain UUID, a prefixed UUID, or a registry-scoped composite string?

  • ✅ Criterion met
  • ❌ Criterion not met
  • 🟡 Partially met or unsure
CriteriaPure UUID (rec.)Prefixed UUIDComposite 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

Example

# Direct lookup by the system's UUID for this organization
GET /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 registry and id query parameters on list routes, so the top-level id doesn’t need to encode any cross-system context.
    • Implementers can use UUIDv7 for time-ordered, index-friendly IDs without the protocol specifying a version.
  • Cons
    • Top-level id duplicates identifiers.systemId.id.
    • UUIDs carry no information about the record on their own, so logs and error messages need an extra lookup for context.

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.
  • 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 id field (Opportunity, Competition, Form, Application, Award, etc.) and would also require updating each model’s id validation to accept the prefixed format. Out of scope for this ADR.

Example

GET /organizations/US-EIN:12-3456789
  • Pros
    • Readable: US-EIN:12-3456789 makes the registry and value visible in logs and URLs.
    • One string carries both registry and value, so no separate field is needed.
  • 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.

Should the identifier collection be a flat array or a keyed map?

CriteriaMap (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🟡

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.
    • otherIds gives a natural extension point for non-standard registries.
  • Cons
    • Differs structurally from org-id.guide, even though codes are shared.
    • otherIds nesting adds one level of indirection for extensions.

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 + id model directly.
    • Well-established in open data standards (OCDS, 360Giving, IATI).
  • 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 scheme cleanly.
    • Multiple IDs per registry appear as duplicate entries with no clear primary.

If the collection is a map, should the keys be raw registry codes or short model-defined aliases?

CriteriacamelCase 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🟡

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.code field 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.

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.

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.

CriteriaNested hybrid (rec.)FlatFully 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

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:

commongrants.org/registries/us-ein.yaml
code: 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}
source: https://org-id.guide/list/US-EIN
  • Pros
    • registry.url gives 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.
    • scope and kind are available inline for filtering and UI decisions (e.g. “show all government-issued IDs”) without a catalog fetch.
    • schema supports per-registry validation in JSON Schema.
    • Required surface stays minimal: only registry.code, registry.url, and id are 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.code vs. a flat identifier.registry).
    • Inline registry fields can drift from the catalog if not kept in sync.
    • scope and kind are technically derivable from registry.code via the catalog, so they are redundant.

Example

{
"ein": {
"registry": "US-EIN",
"registryUrl": "https://commongrants.org/registries/us-ein",
"id": "12-3456789"
}
}
  • Pros
    • Minimal shape: just registry (code), registryUrl, and id.
    • Direct keyed access without extra nesting.
    • registryUrl still gives a clear pointer to the catalog.
  • 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.

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.

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.

CriteriaArray 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
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
    • id is the single clear primary, but allIds covers 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 allIds entirely and just use id.
    • 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 status field.
    • Membership checks require projecting first (ein.allIds.map(v => v.id).includes(x)) instead of a native in / contains() check.
    • The primary id value is duplicated across id and the corresponding allIds entry.

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() / in membership checks against allIds work 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 id value is duplicated across id and the allIds array.

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.

CriteriaWith identifiers (rec.)MinimalFull 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🟡
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.

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.

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?

Should registry codes follow org-id.guide, a uniform namespaced format, or both?

Criteriaorg-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🟡
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.

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.

Who maintains the registry catalog, and how does a registry get promoted from otherIds to a base identifier?

CriteriaHybrid (rec.)org-id.guide onlyCG-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🟡
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:

commongrants.org/registries/us-ein.yaml
code: US-EIN
alias: 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}
source: https://org-id.guide/list/US-EIN
models: [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.
  • 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: platform or kind: system for non-public registries.
    • Getting a new registry added to org-id.guide is external to our release cycle.
  • 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.