Skip to content

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 ​

bash
# 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.

bash
# 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 ​

bash
# 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:

bash
curl -s "$API_URL/search?q=medical%20emergency&limit=20"

Read one campaign ​

bash
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.

bash
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 ​

bash
# 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 ​

bash
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.

bash
# 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.

bash
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 ​

bash
# 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 ​

bash
# 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 times

This route is limited to 5 requests per minute.

Categories and organizations ​

bash
# 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 ​

bash
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.
  • 429 responses carry retryAfter in seconds.

Built with VitePress