Ambassador Program & Portal
Ambassadors are people who carry campaigns to new donors. Anyone signed in can share a personal /r/ referral link; the ambassador role adds a portal for reading what those links did, the right to endorse campaigns, and a place in the public ambassador directory.
This page covers the whole lifecycle: applying, being invited, accepting the invitation, endorsing, and reading the portal.
| Reference section | Operations | About |
|---|---|---|
| Endorsements | 5 | Ambassadors publicly vouching for other people's campaigns, and creators asking ambassadors to promote theirs. |
| Ambassadors | 14 | The ambassador programme: the public directory, invitations and applications, referral links and click tracking, and the ambassador portal under `/me/referrals/*`. |
The role and its permissions
ambassador is a global role. It grants exactly two permissions:
| Permission | Means |
|---|---|
view_own_referral_portal | May open the portal and read their own referral data (/me/referrals/*, /me/campaigns). |
endorse_campaigns | May endorse a published campaign (POST/DELETE /fundraisers/:id/endorse). |
The role is granted by FundlyHub: through an approved application on an existing account, a redeemed invitation, or directly. The profile's account_kind = 'ambassador' badge is display only — it grants nothing.
Applying
POST /ambassador-applications — public, optional auth, 5 requests a minute (the strict limiter).
| Field | Rules |
|---|---|
fullName | 2–120 characters |
email | A valid address, up to 254 characters |
location | 2–160 characters |
socialMedia | Optional; a URL up to 500 characters, or "" |
experience | 50–5,000 characters |
motivation | 50–5,000 characters |
curl -X POST https://api.fundlyhub.org/api/v1/ambassador-applications \
-H 'Content-Type: application/json' \
-d '{
"fullName": "Dana Ortiz",
"email": "dana@example.com",
"location": "Austin, TX",
"socialMedia": "https://instagram.com/dana",
"experience": "Five years organising neighbourhood food drives and school fundraisers.",
"motivation": "I want the campaigns in my city to reach people outside our own circles."
}'| Status | Meaning |
|---|---|
201 | { "received": true } — nothing else, deliberately. |
400 | A field failed validation; error names it, e.g. "motivation: String must contain at least 50 character(s)". |
409 | That address already holds the role, or already has an application in review. |
The form works signed out. When a session is present, the application records who filed it, because a signed-in person can apply on behalf of another address.
FundlyHub reviews each application. Approval has two outcomes, decided at approval time by whether the applicant's address has an account:
- No account — an invitation is created and emailed (next section). The role is granted when it is redeemed during registration.
- Has an account — the role is granted directly and an acceptance email is sent. No invitation, because an existing address can never redeem one.
Invitations
An invitation is a deferred role grant: a single-use token bound to one email address, valid for 14 days. FundlyHub sends invitations; the invited person accepts one with:
| Endpoint | Auth | Purpose |
|---|---|---|
POST /ambassador-invites/redeem | Signed in | { "token": "…" } — accept your own invitation. |
Redeeming. The invitation link opens the sign-up page with an ambassador_invite query parameter. There are two ways the token is spent:
- Email and password sign-up sends it as
ambassadorInviteTokenin thePOST /cognito/signupbody, and the role is granted as the account is created. - Google or Apple sign-up never reaches
/cognito/signup, so the client keeps the token and callsPOST /ambassador-invites/redeemonce signed in.
Either way the token is checked against the address the account was actually created with. A forwarded link redeems for nobody else.
POST /ambassador-invites/redeem answers 200 even when nothing is granted, because a client calls it whenever it holds a token, including right after the sign-up body already spent it:
{ "granted": true }{ "granted": false, "reason": "used" }reason is one of no_token, not_found, used, revoked, expired or email_mismatch. A missing session is 401; a missing token is 400.
Endorsing campaigns
| Endpoint | Auth | Purpose |
|---|---|---|
POST /fundraisers/:id/endorse | endorse_campaigns | { "note"?: "…" } (up to 280 characters). 201 with { endorsement, created: true } the first time; 200 with created: false when you edit the note. |
DELETE /fundraisers/:id/endorse | endorse_campaigns | { "revoked": true }. |
GET /fundraisers/:id/endorsements | Public | { endorsers, viewerHasEndorsed } — the campaign's Endorsed-by list. |
You always endorse as yourself; there is no user id in the path. Only a campaign a visitor could already find can be endorsed — public, and live or ended. Refusals: 404 when the campaign does not exist, 409 when it is not endorsable or is your own. Both write endpoints use the authenticated limiter (100 a minute). Revoking keeps a record; endorsing again later is allowed.
An endorser of a campaign is anyone other than its organizer who either declared an endorsement here or sent at least one real (human-classified) visitor to it through their referral link. Both count, on every surface that shows endorsers.
Creators can ask ambassadors to share a campaign. These endpoints need no permission, only ownership of the campaign:
| Endpoint | Purpose |
|---|---|
GET /ambassadors/selectable?limit= | The picker: ambassadors you can ask (default 24, max 60), with their referred totals. Authenticated because the totals are not public. |
POST /fundraisers/:id/endorsement-requests | { "ambassadorIds": ["…"] } — at most 12 per request; requires a verified email. Ids that are not ambassadors, or are you, are dropped and returned in rejected. |
GET /fundraisers/:id/endorsement-requests | The requests made for your campaign. |
Each asked ambassador gets one email with the campaign's share kit (they can turn these off with notify_endorsement_requests). Requests on a campaign that is not yet public wait, and are sent when it is approved.
Referral links
| Endpoint | Auth | Purpose |
|---|---|---|
POST /referrals/codes | Signed in | { "fundraiser_id"?: "…" } — get or create your code for a campaign (or a profile-level code when omitted). Returns { code }; the share link is https://fundlyhub.org/r/<code>. |
POST /referrals/clicks | Website only | Called by the website when someone opens an /r/ link. Not for integrations. Rate-limited per caller, 120 a minute. |
A donation is attributed to the last human (or unclassified) click on one of your links within 30 days, matched by referral code or visitor id. Bot and link-preview clicks are counted but never attributed. Self-referred gifts are listed in your portal but excluded from your totals.
The portal: /me/referrals/*
All seven endpoints need a signed-in caller with view_own_referral_portal, and every one is scoped to the session — there is no user id in the path.
| Endpoint | Window | Returns (data) |
|---|---|---|
GET /me/referrals/summary | Yes | totals (clicks by traffic type, distinct human visitors, driven gifts, raised_cents, tips_cents, distinct donors, refunds, human conversion rate, currencies), the three-step funnel, and a daily time_series. |
GET /me/referrals/campaigns | Yes | Campaigns you sent traffic to: clicks by type, distinct human visitors, driven gifts, raised_cents, last click. |
GET /me/referrals/campaigns/{fundraiserId} | No (all time) | Your link on one campaign: impressions (visits not identified as a bot), clicks_by_traffic_type, distinct human visitors, driven gifts, distinct donors, funds_attributed (donation + tip per currency, cents), your referral_code / referral_url for it when one exists, and as_of. A campaign you may not open is 404. This is what the bar on a campaign page shows you. |
GET /me/referrals/traffic | Yes | by_source, by_referer, by_medium, traffic_type, plus Google Analytics geography and devices when available (ga_note says why when not). |
GET /me/referrals/gifts | Yes | The attributed donations themselves. ?limit= (default 50, max 200), ?offset=. Also returns total and drivenTotal. |
GET /me/referrals/standing | Yes | Your rank, total_ambassadors, and the leaderboard (capped; truncated says so). |
GET /me/campaigns | No | Campaigns you own, with lifetime totals. |
The from / to window
The five windowed endpoints take ?from= and ?to=, any date string Date parses (ISO-8601 is safest):
todefaults to now;fromdefaults to 28 days beforeto.frommust be beforeto.frommay not be earlier than 2020-01-01 — the floor that "All time" starts at. An earlierfromis refused, not clamped, so a long range is never silently shortened.
Every windowed response echoes the window it used as range: { from, to }. A bad window is 400 with code: "invalid_range" and the reason in error ("from must be before to", "from must be on or after 2020-01-01", …).
curl -H "Authorization: Bearer $FUNDLYHUB_API_KEY" \
"https://api.fundlyhub.org/api/v1/me/referrals/summary?from=2026-09-01T00:00:00Z&to=2026-10-01T00:00:00Z"{
"data": {
"totals": {
"clicks_by_traffic_type": { "human": 412, "bot": 96, "unknown": 7 },
"distinct_human_visitors": 301,
"driven_gifts": 18,
"raised_cents": 214500,
"tips_cents": 21900,
"distinct_donors": 17,
"refunded_count": 0,
"refunded_amount_cents": 0,
"human_conversion_rate": 0.0598,
"currencies": ["usd"]
},
"funnel": [ { "key": "clicks", "count": 515 }, { "key": "human_visitors", "count": 301 }, { "key": "driven_gifts", "count": 18 } ],
"time_series": [ { "date": "2026-09-01", "clicks_human": 12, "clicks_bot": 3, "clicks_unknown": 0, "driven_gifts": 1, "raised_cents": 5000 } ]
},
"range": { "from": "2026-09-01T00:00:00.000Z", "to": "2026-10-01T00:00:00.000Z" }
}Money is integer cents. When currencies has more than one entry, the totals add different currencies together, so say so rather than printing one figure.
/me/referrals/gifts never returns a donor's email address, and an anonymous gift's donor_name is null.
Public reads
| Endpoint | Auth | Purpose |
|---|---|---|
GET /ambassadors?limit= | Public | The directory behind the home-page rail: name, city and money driven. |
GET /platform/ambassadors | Public | Everyone holding the ambassador role, with their figures — the rail on FundlyHub's own profile (/@fundlyhub). It reads the role itself, so it includes ambassadors who have not driven a gift yet. |
GET /users/:id/impact | Public | An ambassador's Impact block on their profile. (/ambassadors/:id/impact was removed.) |
GET /ambassadors/:id/analytics | The ambassador themself | The full per-ambassador analytics. |
Related
- Become an ambassador and Referral links & the portal — the user guides
- Roles & Permissions