Skip to main content

Syncing users

A FounderPass user record represents one member of your community. The Partner API lets you create, list, update, and delete users.

Create or return a user

curl -X POST https://perks.founderpass.com/v1/users \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"email": "alice@example.com",
"external_user_id": "cust_abc123",
"tier": "free",
"user_types": ["Early-stage startup"],
"regions": ["North America"],
"eligibility": {
"eligibility_company_stage": ["seed", "series_a_plus"],
"eligibility_funding": ["vc_backed_only"]
}
}'

If the email already belongs to your partner, the response is the existing user record with already_exists: true. Existing records are returned unchanged; request fields such as tier, user_types, regions, and firebase_uid are only applied when a new user is created. This makes the endpoint idempotent — safe to call from a webhook or batch job.

FieldTypeRequiredNotes
emailstringYesUser's email
external_user_idstringNoYour own user ID for this member. Unique per partner. Lets you attribute events without Firebase — pass it later as the external_user_id resolver on POST /v1/events/track.
firebase_uidstringNoSet if your users authenticate via Firebase
tierenumNofree (default) or premium — drives freemium gating
user_typesarray of UserTypeNoLifecycle stage(s) — e.g. ["Early-stage startup"]
regionsarray of AudienceRegionNoNamed regions — e.g. ["Europe", "North America"]
eligibilityobjectNoThe member's structured eligibility — same eligibility_* arrays a deal carries (see Eligibility criteria). Drives which gated deals the user can see and redeem. See the note below.

:::info user_types and regions are arrays A single user can belong to multiple types or regions — the schema is plural. Pass ["SME"] for a single value. :::

:::tip Declaring eligibility so gated deals are redeemable A deal is only visible/redeemable to a user when the user's eligibility overlaps the deal's on every constrained dimension. Only three dimensions get permissive defaults when omitted — customer_type, company_size, and incorporation — so deals that constrain just those work out of the box. The other four (company_stage, business_type, geography, funding) stay empty unless you supply them, and an empty user value does not match a deal that constrains that dimension. So a deal gated on company_stage or funding (e.g. "Seed–Series A, VC-backed only") returns 404 from GET /v1/deals/{id}/instructions until the user carries a matching value. Set those via the eligibility object on create or PATCH /v1/users/{id}. Eligibility is self-attested — it gates visibility; deals needing verification run an approval step at redemption. :::

Response

{
"id": "018e4b2a-0000-7def-abcd-000000000099",
"email": "alice@example.com",
"external_user_id": "cust_abc123",
"tier": "free",
"user_types": ["Early-stage startup"],
"regions": ["North America"],
"eligibility_customer_type": ["new_customers_only", "existing_customers_eligible"],
"eligibility_company_stage": ["seed", "series_a_plus"],
"eligibility_company_size": ["1_10", "11_50", "51_200", "200_plus"],
"eligibility_incorporation": ["registered_company_required", "sole_traders_allowed"],
"eligibility_funding": ["vc_backed_only"],
"sign_in_url": "https://your-slug.perks.founderpass.com/sign-in?token=...",
"already_exists": false,
"created_at": "2026-05-26T16:00:00Z"
}

sign_in_url is a magic link your member can click to land directly inside their portal — useful for onboarding emails.

:::note Eligibility is nested on input, flat on output You send eligibility as a nested eligibility object (above), but responses return the resolved values as flat eligibility_* keys — the same shape a deal and the User object use everywhere else in the API (GET/PATCH /v1/users, GET /v1/deals). The flat values are the resolved set: what you sent, plus the baseline defaults filled in for customer_type / company_size / incorporation. :::

List users

curl "https://perks.founderpass.com/v1/users?page=1&per_page=50" \
-H "Authorization: Bearer $TOKEN"

Supports standard pagination, plus an optional tier=free|premium filter.

Update a user

Use PATCH /v1/users/{id} to update existing user fields. Omitted fields remain unchanged.

curl -X PATCH https://perks.founderpass.com/v1/users/{id} \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"tier": "premium",
"external_user_id": "cust_abc123",
"user_types": ["Growth-stage startup"],
"regions": ["Europe", "North America"],
"eligibility": {
"eligibility_company_stage": ["series_a_plus"],
"eligibility_funding": ["vc_backed_only", "accelerator_only"]
}
}'

You can update tier, user_types, regions, firebase_uid, external_user_id, and eligibility. Re-running POST /v1/users for an existing email returns the existing record and does not refresh those fields, so use PATCH to set or change a member's external_user_id or eligibility after creation. external_user_id must be unique within your partner — reusing one that already belongs to another user returns 409 Conflict.

:::info eligibility on PATCH is a per-dimension merge Each dimension inside eligibility is independent: omitted dimensions are left unchanged, and only the ones you send are updated (sending an explicit empty array — e.g. "eligibility_funding": [] — clears that one). So a partial update like { "eligibility": { "eligibility_company_stage": ["series_a_plus"] } } changes only company_stage and preserves everything else. Unlike create, PATCH never re-applies the permissive defaults, so a partial sync can't accidentally broaden a member you previously narrowed. :::

Delete a user

curl -X DELETE https://perks.founderpass.com/v1/users/{id} \
-H "Authorization: Bearer $TOKEN"

Common patterns

Daily reconciliation

Pull users from your source of truth nightly and create any missing records in FounderPass. Use PATCH /v1/users/{id} for records whose tier, type, region, or Firebase UID changed.

Webhook-driven

When a new user signs up, fire a server-side POST /v1/users. When their tier or audience attributes change, send PATCH /v1/users/{id}. When they leave, DELETE.

:::tip Backfill in chunks For initial migrations of large communities, use per_page=100 on GET /v1/users to verify which users already exist, then create the missing users in batches of ~50 with a short delay between batches. :::

See the full schema and every field in the Users API Reference.