Skip to content

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

  1. Discover. The platform fetches the nonprofit's /.well-known/ucp, resolves services["dev.ugp.giving"] to the provider endpoint, and reads the dev.ugp.giving.donations config to learn supported_rails and address_scope.
  2. Create the batch. The platform POSTs a create_batch_request to /donation-batches with an external_batch_id (its idempotency key) and an array of donations. The provider validates each donation, sums the accepted amounts, and responds 201 with a batch_response whose status is created. The response includes a payment_address — the bank destination the platform must fund — and a payment_reference equal to the batch_id.
  3. Initiate payment. The platform sends funds to the payment_address over one of the supported rails and POSTs /donation-batches/{batch_id}/status with { "status": "payment_initiated", "rail": "...", "payment_trace": { ... } }.
  4. 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.
  5. 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.
  6. Reconcile. The service matches the deposit to the batch (see Reconciliation) and transitions to reconciled, populating the reconciliation block on the batch_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_address issued; 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 via POST /donation-batches/{batch_id}/status, whose status enum is limited to payment_initiated, in_transit, and canceled precisely because those are the only transitions a platform may assert.
  • The service owns settled, reconciled, and failed. 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), optional beneficiary_name.
  • supported_rails — the rails this destination can receive (ach, rtp, fednow, wire).
  • wire_detailsswift_bic, bank_name, bank_address for wires.
  • payment_referenceequals the batch_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:

  1. Which platform? The deposit lands in a per-platform destination account, so the destination account number identifies the originating platform.
  2. Which batch? The payment_reference carried with the payment equals the batch_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" } ] }