Skip to content

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.

  1. Register on publish (push). When a nonprofit adds the dev.ugp.giving service to its /.well-known/ucp, the party that published it calls POST /index with 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.
  2. 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.
  3. 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 records signed / 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: reachable records 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.