Donations API
Create donations, look up receipts, and read a donor's own giving history.
Money is in cents
amount, tip_amount, and fee_amount are integers of the smallest currency unit (cents for USD). A $100.00 donation is amount: 10000.
Create Donation
POST /api/v1/donations 🔒 Requires Authentication (verified email)
Creates a donation row in pending status. Payment is captured separately via the Payments API / POST /payments/confirm; the donation's payment_status is only ever advanced to paid/refunded by the Stripe-verified confirm + webhook paths. For security, any donor_user_id in the body is ignored — the donor is always the authenticated user (or null for guests).
const API_BASE = 'https://api.fundlyhub.org/api/v1';
const response = await fetch(`${API_BASE}/donations`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
fundraiser_id: '123e4567-e89b-12d3-a456-426614174000',
amount: 10000, // $100.00 in cents (required, > 0)
currency: 'USD', // defaults to USD
tip_amount: 1500, // optional platform tip in cents (default 0)
donor_name: 'Jane Smith',
donor_email: 'jane@example.com',
is_anonymous: false, // default false
comment: 'Great cause! Keep up the excellent work.' // optional, max 1000 chars
})
});
const { data } = await response.json();
console.log('Donation created:', data.id, 'status:', data.payment_status);Returns 201 with { data: <donation row> }. Validation failures return 400 with { error: 'Validation error', details: [...] }.
Look up a receipt
GET /api/v1/donations/receipt/:receiptId
Public receipt lookup keyed by the Stripe payment-intent id (pi_…) or invoice id (in_…) — both high-entropy capability tokens, so this stays unauthenticated for guest donors. Returns an explicit field allowlist (no donor PII beyond what the receipt needs).
const API_BASE = 'https://api.fundlyhub.org/api/v1';
const receiptId = 'pi_3AbCdEf...';
const response = await fetch(`${API_BASE}/donations/receipt/${receiptId}`);
const { data } = await response.json();
console.log('Amount:', data.amount / 100, data.currency);Email a receipt
POST /api/v1/donations/receipt/email
Sends a receipt email. Optional auth — donors may not be logged in. Public rate-limited.
await fetch(`${API_BASE}/donations/receipt/email`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
recipient_email: 'jane@example.com',
receipt_data: { /* receipt fields */ }
})
});Get a fundraiser's donations
GET /api/v1/fundraisers/:fundraiserId/donations
Public list of donations for a fundraiser (anonymous donors are redacted). Supports ?limit and ?offset.
const fundraiserId = '123e4567-e89b-12d3-a456-426614174000';
const response = await fetch(
`${API_BASE}/fundraisers/${fundraiserId}/donations?limit=10`
);
const { data } = await response.json();A donor's own giving history
Authenticated donors read their own history under /donor/me/*:
| Endpoint | Purpose |
|---|---|
GET /donor/me/summary | Aggregate giving stats. |
GET /donor/me/donations | Paginated giving history with year / status / fundraiserId filters. |
GET /donor/me/annual-statement | Year-end giving summary as PDF or CSV. |
const response = await fetch(
`${API_BASE}/donor/me/donations?page=1&limit=20&year=2026`,
{ credentials: 'include' }
);
const { data, pagination } = await response.json();Removed endpoints
GET /donations/:id and PATCH /donations/:id/status were removed for security. The raw single-donation read leaked donor PII and ignored is_anonymous; the status PATCH had no auth and trusted a client-supplied payment status. Status transitions are owned exclusively by the Stripe-verified POST /payments/confirm path and the Stripe webhook. Admin reads go through GET /admin/donations/:id; donors use /donor/me/* and the receipt-token route above.
Response Codes
201— Donation created200— Success400— Validation error401— Authentication required404— Not found429— Rate limit exceeded500— Server error