Registry Service — dev.ugp.registry¶
The registry is a directory that sits in front of discovery. UGP discovery
answers "given a domain, what can this nonprofit do?" (GET /.well-known/ucp).
The registry answers the question that comes first: "given a name, EIN, or
website, what is the domain?" It resolves a search term to a nonprofit's
/.well-known/ucp, which the caller then discovers and verifies directly.
ugp.dev operates the canonical registry and offers search for free. The
contract below is open: anyone may run a registry, and a platform may query any
registry identically.
Try it live
The canonical registry runs at registry.ugp.dev —
search and browse the directory in your browser, or call the REST API directly at
https://registry.ugp.dev/api/registry/v1.
The service¶
The registry is a UCP service like dev.ugp.giving, but it is advertised by a
registry operator, not by the nonprofit. An operator declares it in its own
/.well-known/ucp. This is exactly what ugp.dev publishes at
https://ugp.dev/.well-known/ucp:
jsonc
"services": {
"dev.ugp.registry": [
{
"version": "2026-06-25",
"transport": "rest",
"endpoint": "https://registry.ugp.dev/api/registry/v1",
"spec": "https://ugp.dev/2026-06-25/specification/registry/",
"schema": "https://ugp.dev/2026-06-25/services/registry/rest.openapi.json"
}
]
}
A platform discovers a registry the same way it discovers anything else: fetch
the operator's profile, resolve services["dev.ugp.registry"], and use the
binding's endpoint as the base URL for the operations below. Try it:
bash
curl https://ugp.dev/.well-known/ucp
The three REST endpoints¶
| Method | Path | operationId | Purpose |
|---|---|---|---|
GET |
/search |
search |
Resolve nonprofits by name, EIN, or website. Read-only; MAY be unauthenticated. |
POST |
/index |
index |
Submit a /.well-known/ucp for (re)indexing. Idempotent on well_known_url. |
DELETE |
/index/{domain} |
remove |
Deregister a domain. |
Schema references resolve against
source/schemas/registry/search.json
($id: https://ugp.dev/schemas/registry/search.json), which holds the $defs
consumed by the REST binding: registry_entry, search_query,
search_response, index_request, and index_response.
The golden rule: an entry is a projection, never the source of truth¶
This is the load-bearing design decision. Authority always lives in the
nonprofit's signed /.well-known/ucp. A registry_entry carries only what is
needed to disambiguate and rank a result — name, EINs, website, category, logo —
plus the one field that matters: the well_known_url.
A caller MUST resolve and verify that well_known_url (signature, capabilities,
endpoint) before it transacts. The money path never goes through the
registry. A stale, wrong, or even malicious entry can mislead a search
result, but it cannot misdirect funds, because settlement still flows through
verified discovery. This is precisely what makes a registry safe to be free,
public, and federated.
It also means submitters never push profile field data into the index. They only ever say "(re)read my well-known." The registry owns all derivation, so an entry physically cannot drift from the truth in a way that matters.
Indexing lifecycle¶
A nonprofit is indexed, and kept fresh, through three complementary mechanisms. Together they make freshness fast where there's cooperation and eventual where there isn't.
- Register on publish (push). When a nonprofit adds the
dev.ugp.givingservice to its/.well-known/ucp, the party that published it callsPOST /indexwith the well-known URL. In practice this is almost always the provider, since it manages the profile and hosts the endpoint; a self-hosting nonprofit submits the same request directly. - Re-index on edit (push). Any change to the profile — a new display name,
an added EIN — triggers the same
POST /index. The registry re-fetches the well-known and re-derives the entry, so the new name propagates to search. No field-level sync is involved. - Re-crawl (pull backstop). The registry periodically re-fetches every
known well-known with a conditional
GET(ETag /Last-Modified) to catch changes from providers that don't push, and to prune entries whose well-known now returns 404. Freshness therefore does not depend on cooperation: pushes make it fast, the crawl makes it eventual.
A profile that does not advertise dev.ugp.giving is not indexed
(status: no_giving_service) — the registry is a directory of giving-enabled
nonprofits.
Trust, verification, and ranking¶
Because the registry is public and EINs are sensitive, each entry records what the registry could verify. These flags are advisory — for ranking and display — and never a substitute for verifying the authoritative profile.
- Domain control needs no separate proof: the well-known is served on the nonprofit's own domain, which is UCP's existing trust anchor.
- Signature: the registry verifies the profile against its
signing_keys(RFC 9421) and recordssigned/signature_verified. - EIN: the registry MAY cross-check EINs against an authoritative source
(IRS Pub 78 / Business Master File) and record
ein_verified, flagging collisions where two domains claim one EIN — a classic impersonation signal. - Reachability:
reachablerecords whether the last crawl succeeded.
Ranking favors the strongest resolution key first — exact ein, then exact
website/domain, then fuzzy q — and breaks ties toward entries that are
signed, EIN-verified, and reachable.
Federation¶
The contract is standardized so registries are interchangeable. An operator MAY
accept the same POST /index submissions, MAY crawl independently, and MAY
replicate from or crawl other registries. ugp.dev is the canonical free
default; a platform that prefers its own index can run one and point at it
without changing any caller code.
Example¶
A search for a nonprofit by name, and the pointer it resolves to:
jsonc
// GET /search?q=eastside
{
"ucp": { "version": "2026-06-25", "status": "success" },
"query": { "q": "eastside" },
"results": [
{
"name": "Eastside Shelter",
"legal_name": "Eastside Community Shelter, Inc.",
"eins": ["12-3456789"],
"website": "https://eastsideshelter.org",
"domain": "eastsideshelter.org",
"well_known_url": "https://eastsideshelter.org/.well-known/ucp",
"giving_endpoint": "https://giving-provider.example.com/api/ugp/v1/orgs/eastside-shelter",
"category": "Housing & Shelter",
"ntee_code": "L41",
"logo": { "type": "image", "url": "https://eastsideshelter.org/assets/logo.png" },
"verification": {
"signed": true,
"signature_verified": true,
"ein_verified": true,
"reachable": true
},
"indexed_at": "2026-06-25T12:00:00Z",
"score": 0.97
}
],
"pagination": { "has_next_page": false, "total_count": 1 }
}
The platform takes well_known_url, runs discovery against it, verifies the
signature, resolves the dev.ugp.giving endpoint, and only then creates a
donation batch — exactly the flow described in
Overview and Donations.