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.
| Field | Type | Required | Notes |
|---|---|---|---|
email | string | Yes | User's email |
external_user_id | string | No | Your 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_uid | string | No | Set if your users authenticate via Firebase |
tier | enum | No | free (default) or premium — drives freemium gating |
user_types | array of UserType | No | Lifecycle stage(s) — e.g. ["Early-stage startup"] |
regions | array of AudienceRegion | No | Named regions — e.g. ["Europe", "North America"] |
eligibility | object | No | The 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.