Donations Capability — dev.ugp.giving.donations¶
The donations capability defines how a platform remits batches of donations it
has collected to a nonprofit, over bank rails. It is validated by
source/schemas/giving/donations.json
($id: https://ugp.dev/schemas/giving/donations.json), which holds the
$defs consumed by the REST binding: batch_status, payment_address,
create_batch_request, batch_response, status_update_request, and
batch_state_response. Individual donations are validated by
source/schemas/giving/types/donation.json.
The capability is declared in the nonprofit profile with config advertising the rails it accepts and how payment addresses are scoped:
jsonc
"dev.ugp.giving.donations": [
{
"version": "2026-06-25",
"spec": "https://ugp.dev/specification/donations",
"schema": "https://ugp.dev/schemas/giving/donations.json",
"config": {
"supported_rails": ["ach", "rtp", "fednow", "wire"],
"address_scope": "per_platform",
"currency": "USD"
}
}
]
End-to-end workflow¶
- Discover. The platform fetches the nonprofit's
/.well-known/ucp, resolvesservices["dev.ugp.giving"]to the provider endpoint, and reads thedev.ugp.giving.donationsconfig to learnsupported_railsandaddress_scope. - Create the batch. The platform
POSTs acreate_batch_requestto/donation-batcheswith anexternal_batch_id(its idempotency key) and an array ofdonations. The provider validates each donation, sums the accepted amounts, and responds201with abatch_responsewhose status iscreated. The response includes apayment_address— the bank destination the platform must fund — and apayment_referenceequal to thebatch_id. - Initiate payment. The platform sends funds to the
payment_addressover one of the supported rails andPOSTs/donation-batches/{batch_id}/statuswith{ "status": "payment_initiated", "rail": "...", "payment_trace": { ... } }. - In transit (optional). For rails with a settlement delay (e.g. ACH), the
platform MAY report
{ "status": "in_transit" }once the payment has left its bank. - Settle. When the deposit lands in the destination account, the service
transitions the batch to
settled. The platform was the originator of funds; it does not drive this transition. - Reconcile. The service matches the deposit to the batch (see
Reconciliation) and transitions to
reconciled, populating thereconciliationblock on thebatch_state_response.
At any point before funds leave its bank, the platform may
POST { "status": "canceled", "reason": "..." } to abandon the batch.
Batch lifecycle state machine¶
created ──► payment_initiated ──► in_transit (optional) ──► settled ──► reconciled
│ │ │
└────────────────┴─────────────────────┴──► canceled (any state may ──► failed)
created— batch accepted;payment_addressissued; awaiting funding.payment_initiated— platform has begun the funding payment.in_transit— funds have left the platform's bank (used by delayed rails).settled— funds have landed in the destination account.reconciled— deposit matched to the batch; terminal success.canceled— batch abandoned before funding; terminal.failed— payment or settlement failed; terminal.
Ownership of transitions:
- The platform owns transitions up to funds leaving its bank:
created → payment_initiated → in_transit, and→ canceled. These are driven viaPOST /donation-batches/{batch_id}/status, whosestatusenum is limited topayment_initiated,in_transit, andcanceledprecisely because those are the only transitions a platform may assert. - The service owns
settled,reconciled, andfailed. These reflect what actually happened at the bank and cannot be self-reported by the platform.
Idempotency via external_batch_id¶
create_batch_request.external_batch_id is the platform's stable identifier for
the batch and serves as the application-level idempotency key. Resubmitting a
create_batch with the same external_batch_id MUST return the existing batch
rather than creating a duplicate or issuing a new payment_address. (The
transport-level Idempotency-Key header guards individual HTTP retries; the
external_batch_id guards the batch identity across sessions.) Within a batch,
each donation's external_id is the idempotency/reconciliation key for that
single gift.
Per-platform payment addresses (address_scope: per_platform)¶
With address_scope: "per_platform", the provider issues a distinct bank
destination per platform. This is what makes hands-off reconciliation possible:
because each platform funds a different destination account, an incoming deposit
self-identifies its originating platform by destination account number — no
out-of-band coordination is required to know which platform a deposit came from.
A payment_address carries:
account_number,routing_number(^\d{9}$),account_type(checking/savings), optionalbeneficiary_name.supported_rails— the rails this destination can receive (ach,rtp,fednow,wire).wire_details—swift_bic,bank_name,bank_addressfor wires.payment_reference— equals thebatch_id; the platform includes it with the payment so the deposit ties back to a specific batch.
Reconciliation¶
Reconciliation is two-dimensional and requires no manual matching:
- Which platform? The deposit lands in a per-platform destination account, so the destination account number identifies the originating platform.
- Which batch? The
payment_referencecarried with the payment equals thebatch_id, tying the deposit to a specific batch.
When a deposit settles, the service records a reconciliation block on the
batch_state_response: deposit_matched, deposited_amount, deposited_at,
and a discrepancy of none, amount_mismatch, unmatched_deposit, or
partial. A clean match (discrepancy: none) moves the batch to reconciled.
Example /.well-known/ucp for a nonprofit¶
The following is a complete discovery profile for the sample nonprofit
eastsideshelter.org. Its giving endpoint is hosted by the provider Chariot.
Note payment_handlers: {} (giving uses bank rails, not tokenized payment) and
the single EC P-256 signing key. The same document is available as
example-profile.json.
json
{
"ucp": {
"version": "2026-06-25",
"services": {
"dev.ugp.giving": [
{
"version": "2026-06-25",
"transport": "rest",
"endpoint": "https://chariot.app/api/ugp/v1/orgs/eastside-shelter",
"spec": "https://ugp.dev/specification/overview",
"schema": "https://ugp.dev/services/giving/rest.openapi.json"
}
]
},
"capabilities": {
"dev.ugp.giving.profile": [
{
"version": "2026-06-25",
"spec": "https://ugp.dev/specification/profile",
"schema": "https://ugp.dev/schemas/giving/profile.json",
"config": {
"name": "Eastside Shelter",
"legal_name": "Eastside Shelter Foundation, Inc.",
"eins": ["12-3456789"],
"tax_deductible_status": "501c3",
"mission": "Providing safe shelter, meals, and housing-placement services to families experiencing homelessness on the east side.",
"ntee_code": "L41",
"category": "Human Services",
"website": "https://eastsideshelter.org",
"logo": {
"type": "image",
"url": "https://eastsideshelter.org/assets/logo.png",
"alt_text": "Eastside Shelter logo"
},
"brand_colors": {
"primary": "#1f6f54",
"secondary": "#0d3b2e",
"accent": "#f4a300"
},
"address": {
"street_address": "120 Birch Avenue",
"address_locality": "Eastside",
"address_region": "WA",
"postal_code": "98004",
"address_country": "US"
},
"contact": {
"email": "give@eastsideshelter.org",
"phone_number": "+1-425-555-0142"
},
"social_links": [
{
"type": "website",
"url": "https://eastsideshelter.org",
"title": "Website"
}
],
"designations": [
{ "id": "general", "name": "General Fund", "default": true },
{ "id": "meals", "name": "Meals Program" },
{ "id": "housing", "name": "Housing Placement" }
]
}
}
],
"dev.ugp.giving.donations": [
{
"version": "2026-06-25",
"spec": "https://ugp.dev/specification/donations",
"schema": "https://ugp.dev/schemas/giving/donations.json",
"config": {
"supported_rails": ["ach", "rtp", "fednow", "wire"],
"address_scope": "per_platform",
"currency": "USD"
}
}
]
},
"payment_handlers": {}
},
"signing_keys": [
{
"kid": "eastside-2026-06",
"kty": "EC",
"crv": "P-256",
"x": "f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU",
"y": "x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0",
"use": "sig",
"alg": "ES256"
}
]
}