cURL Examples
Command-line examples for testing and integrating with the FundlyHub API.
Before you copy anything
Field names are snake_case and every amount is integer cents, everywhere — amount_cents: 5000 is $50.00. The fields carry the unit in their names (amount_cents, tip_amount_cents, goal_amount_cents), so there is nothing to remember per endpoint.
Setup
# The API base. Note: /health and /ready live at the HOST root, not under /api/v1.
export API_URL="https://api.fundlyhub.org/api/v1"
export COOKIE_JAR="cookies.txt"
# Liveness probe — no /api/v1 prefix
curl -i "https://api.fundlyhub.org/health"
# What the version root advertises
curl -s "$API_URL"Authentication
Sessions are httpOnly cookies set by AWS Cognito sign-in. Use a cookie jar to persist them.
# Register a new user
curl -X POST "$API_URL/cognito/signup" \
-H "Content-Type: application/json" \
-c "$COOKIE_JAR" \
-d '{
"email": "user@example.com",
"password": "SecureP@ssw0rd!",
"name": "John Doe"
}'
# Confirm registration with the emailed code
curl -X POST "$API_URL/cognito/confirm" \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"code": "123456"
}'
# → {"message":"Email verified successfully. You can now sign in."}
# Sign in — cookies land in the jar
curl -X POST "$API_URL/cognito/signin" \
-H "Content-Type: application/json" \
-c "$COOKIE_JAR" -b "$COOKIE_JAR" \
-d '{
"email": "user@example.com",
"password": "SecureP@ssw0rd!"
}'
# → {"message":"Signin successful","user":{…},"expiresIn":3600}
# Who am I?
curl -b "$COOKIE_JAR" "$API_URL/cognito/me"Sign-in, sign-up and password reset share a 10 requests/minute limit.
Fundraisers
List and filter
# Active campaigns
curl -s "$API_URL/fundraisers?status=active&limit=20"
# Filter by category — the integer id, the slug, or the display name all work.
# The parameter is `category`, NOT `category_id`, and it is not a UUID.
curl -s "$API_URL/fundraisers?category=medical&status=active"
# Projects rather than ordinary fundraisers
curl -s "$API_URL/fundraisers?is_project=true&status=active"
# Response: {"data":[…],"pagination":{"limit":20,"offset":0,"total":137}}There is no search parameter on this endpoint for public callers. Use the search endpoint:
curl -s "$API_URL/search?q=medical%20emergency&limit=20"Read one campaign
curl -s "$API_URL/fundraisers/slug/help-support-local-food-bank"
curl -s "$API_URL/fundraisers/123e4567-e89b-12d3-a456-426614174000"
# → {"data":{…}}
# Is a slug free? (unwrapped: {"available":true} / {"available":false,"suggestion":"…-2"})
curl -s "$API_URL/fundraisers/check-slug/help-support-local-food-bank"Create a campaign
Requires a session and a verified email — except a "status": "draft" create, which an unverified account may make (up to 5 drafts). Always send status: without it the publish checks run. title, slug and goal_amount_cents are the required fields — description, goal and category_id are not fields this API has, and are dropped silently if you send them.
curl -X POST "$API_URL/fundraisers" \
-b "$COOKIE_JAR" -c "$COOKIE_JAR" \
-H "Content-Type: application/json" \
-d '{
"title": "Help Support Local Food Bank",
"slug": "help-support-local-food-bank",
"goal_amount_cents": 500000,
"summary": "Raising funds to support families",
"story_html": "<p>Our local food bank serves 400 families a week…</p>",
"category": "community",
"location": "Austin, TX",
"tags": ["community", "food"],
"end_date": "2026-12-31",
"status": "draft"
}'
# → 201 {"data":{…,"status":"draft"}}goal_amount_cents is in integer cents (500000 is $5,000.00). Read data.status on the response — asking for "status": "active" can still come back as pending when the automated review sends the campaign to a moderator, and that is a 201, not an error.
Update and delete
# PATCH, not PUT
curl -X PATCH "$API_URL/fundraisers/$FUNDRAISER_ID" \
-b "$COOKIE_JAR" \
-H "Content-Type: application/json" \
-d '{"goal_amount_cents": 750000, "summary": "Updated summary"}'
# → {"data":{…}}
# Soft delete — 200 with a body, not 204. Refused with 400 once money is in.
curl -X DELETE "$API_URL/fundraisers/$FUNDRAISER_ID" -b "$COOKIE_JAR"
# → {"success":true,"campaignId":"…","slug":"…","deletedAt":"…"}Campaign statistics
curl -s "$API_URL/fundraisers/$FUNDRAISER_ID/stats"
# → {"data":{"fundraiser_id":"…","title":"…","goal_amount_cents":500000,
# "total_raised_cents":125000,"total_tips_cents":7500,
# "donation_count":23,"donor_count":21,"percentage_funded":25}}
# Every money field here is integer cents: 125000 = $1,250.00 raised
# against a $5,000.00 goal.There is no batch stats endpoint — no POST /fundraisers/stats. Request one campaign at a time, or read total_raised_cents / donor_count straight off the listing rows.
Taking a payment
The money path is two calls. Everything here is integer cents and snake_case.
# 1. Create the PaymentIntent. `fundraiser_id` — NOT fundraiserId — is what
# routes the money to the campaign. Omit or misspell it and the charge
# lands on the platform balance with no attribution.
curl -X POST "$API_URL/payments/create-intent" \
-H "Content-Type: application/json" \
-H "x-recaptcha-token: $RECAPTCHA_TOKEN" \
-d '{
"amount_cents": 5000,
"currency": "usd",
"fundraiser_id": "123e4567-e89b-12d3-a456-426614174000",
"tip_amount_cents": 500,
"donor_email": "jane@example.com",
"donor_name": "Jane Smith",
"is_anonymous": false
}'
# → {"client_secret":"pi_…_secret_…","payment_intent_id":"pi_…",
# "amount_cents":5500,"stripe_fee_cents":190,"net_amount_cents":5310}
# (all cents; `amount_cents` is the total charge = amount_cents + tip_amount_cents)
# 2. …confirm client_secret with Stripe.js in the browser…
# 3. Settle the donation row
curl -X POST "$API_URL/payments/confirm" \
-H "Content-Type: application/json" \
-d '{"payment_intent_id": "pi_3AbCdEf…"}'
# → {"data":{…settled donation row…}}The response keys are client_secret and payment_intent_id. There is no clientSecret and no paymentIntentId.
Donations
Record a donation
POST /donations is bookkeeping — it creates a pending row and does not charge anything. It needs a session with a verified email. Amounts are integer cents, the same as the payments endpoint above.
curl -X POST "$API_URL/donations" \
-b "$COOKIE_JAR" \
-H "Content-Type: application/json" \
-H "x-recaptcha-token: $RECAPTCHA_TOKEN" \
-d '{
"fundraiser_id": "123e4567-e89b-12d3-a456-426614174000",
"amount_cents": 5000,
"currency": "USD",
"tip_amount_cents": 500,
"donor_name": "Jane Smith",
"donor_email": "jane@example.com",
"comment": "Great cause!",
"is_anonymous": false
}'
# → 201 {"data":{…,"payment_status":"pending"}}The donor's note is comment. A message key is not read. donor_user_id is ignored — the donor is always the authenticated profile.
Read donations
# A campaign's public donor wall — paid donations only, net cents,
# anonymous donors redacted
curl -s "$API_URL/fundraisers/$FUNDRAISER_ID/donations?limit=10&offset=0"
# → {"data":[…],"pagination":{"limit":10,"offset":0,"total":23}}
# The landing-page recent-gifts feed
curl -s "$API_URL/donations/recent?limit=12"
# Your own giving history. There is no /donations/me — it is /donor/me/*.
curl -b "$COOKIE_JAR" "$API_URL/donor/me/donations?page=1&limit=20&year=2026"
curl -b "$COOKIE_JAR" "$API_URL/donor/me/summary"GET /donations/:id no longer exists — it was removed for leaking donor PII. A donor reads a single gift through the receipt route below or /donor/me/*.
Receipts
# Public receipt lookup by Stripe PaymentIntent id
curl -s "$API_URL/donations/receipt/pi_3AbCdEf…"
# Mail a receipt. Exactly two fields: receipt_id and recipient_email.
# There is no receiptId, no email, and no receipt_data — every figure in the
# message is read from the donation itself.
curl -X POST "$API_URL/donations/receipt/email" \
-H "Content-Type: application/json" \
-d '{
"receipt_id": "pi_3AbCdEf…",
"recipient_email": "jane@example.com"
}'
# → 200 sent, 202 queued, 404 unknown/unpaid, 429 mailed too many timesThis route is limited to 5 requests per minute.
Categories and organizations
# All active categories — {"data":[{id,slug,name,description,icon,color,display_order}]}
curl -s "$API_URL/categories"
# One category, by integer id or slug
curl -s "$API_URL/categories/medical"
# Per-category stats: {"data":{"category_name","organization_count","fundraiser_count","total_raised_cents"}}
curl -s "$API_URL/categories/medical/stats"
# Every category's stats — an ARRAY, and this one says campaign_count
curl -s "$API_URL/categories/stats"
# Public organizations (approved/verified only)
curl -s "$API_URL/organizations?limit=20"
# One organization, by UUID or slug
curl -s "$API_URL/organizations/local-food-bank-a1b2c3"
# Org stats — camelCase, and NOT wrapped in "data"
curl -s "$API_URL/organizations/local-food-bank-a1b2c3/stats"
# → {"campaignCount":4,"totalCampaignCount":7,"totalFundsRaisedCents":45600000,
# "uniqueDonorCount":312,"followerCount":88}Reading errors
curl -s -X POST "$API_URL/fundraisers" \
-b "$COOKIE_JAR" -H "Content-Type: application/json" \
-d '{"title":"No slug here"}'
# → 400 {"error":"Validation error","details":[{"path":["slug"],…}]}{ "error": … , "details": [...] }— Zod validation issues.{ "error": "Cannot publish fundraiser", "blockers": [...] }— a publish gate; the array names what is missing.429responses carryretryAfterin seconds.