People API
Create, update, and manage user profiles with the Flameup People API
Overview
The People API allows you to manage user profiles in Flameup. Each person represents a user in your system with their contact information, attributes, and engagement history.
people:read for GET requests, people:write for POST/PUT requests, people:delete for DELETE requests. Note: API keys automatically target the workspace they were created in.Person Object
A person in Flameup has the following structure:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"workspace_id": "ws_abc123",
"userId": "user_12345",
"email": "jane@example.com",
"traits": {
"first_name": "Jane",
"last_name": "Doe",
"plan": "premium",
"company": "Acme Inc"
},
"status": "active",
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-20T14:45:00Z"
}
Field Reference
| Field | Type | Description |
|---|---|---|
id | UUID | Flameup's internal identifier (read-only) |
workspace_id | string | Workspace identifier (read-only) |
userId | string | Your unique identifier for this user (required, max 255 chars) |
email | string | User's email address (optional, max 254 chars) |
phone | string | User's phone number (optional, max 50 chars) |
anonymousId | string | Anonymous identifier for merging anonymous activity (transient, not persisted) |
traits | object | Custom user attributes. Keys must match ^[a-zA-Z][a-zA-Z0-9_]*$ and be ≤255 chars; string values ≤10,000 chars; arrays ≤1,000 elements; objects ≤100 properties |
status | string | Consent state — active, unsubscribed, bounced, or suppressed. Always present on reads (read-only here) |
unsubscribed_at | timestamp | When the person unsubscribed. Omitted unless set (read-only) |
unsubscribe_reason | string | Why the person unsubscribed, max 255 chars. Omitted unless set (read-only) |
created_at | timestamp | When the person was created (read-only) |
updated_at | timestamp | When the person was last updated (read-only) |
status is serialized on every person read — Get, List,
Search, the full-person Create/Upsert echo, and each batch data object — and is echoed
verbatim from the stored row (new people default to active).
unsubscribed_at and unsubscribe_reason are only present once the person has
unsubscribed. None of the three are writable through this API-key surface;
consent state is set from the dashboard.^[a-zA-Z][a-zA-Z0-9_]*$), so dots, spaces, hyphens and leading digits are
rejected — signup.source, plan tier and 2fa_enabled all fail. The keys
id, workspace_id, user_id, anonymous_id, created_at and updated_at
are reserved and rejected outright. Values may nest at most 10 levels deep.
A violation returns 400 on Create, Upsert and Update; inside a batch it fails
just that record's entry.Create a Person
Create a new person profile.
Endpoint: POST /api/v1/people
const response = await fetch(
`https://api.flameup.ai/api/v1/people`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${API_KEY}`
},
body: JSON.stringify({
userId: 'user_12345', // Required: your unique ID
email: 'jane@example.com', // Optional but recommended
phone: '+15551234567', // Optional
traits: {
first_name: 'Jane',
last_name: 'Doe',
plan: 'premium',
company: 'Acme Inc'
}
})
}
);
const person = await response.json();
response = requests.post(
f'https://api.flameup.ai/api/v1/people',
headers={
'Content-Type': 'application/json',
'Authorization': f'Bearer {API_KEY}'
},
json={
'userId': 'user_12345',
'email': 'jane@example.com',
'phone': '+15551234567',
'traits': {
'first_name': 'Jane',
'last_name': 'Doe',
'plan': 'premium',
'company': 'Acme Inc'
}
}
)
person = response.json()
curl -X POST "https://api.flameup.ai/api/v1/people" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your_api_key" \
-d '{
"userId": "user_12345",
"email": "jane@example.com",
"traits": {
"first_name": "Jane",
"last_name": "Doe",
"plan": "premium",
"company": "Acme Inc"
}
}'
userId. Database primary keys or UUIDs work well.userId, user_id, or external_id — these are aliases for the same value.people:read. A
key holding it gets the full person object back. A key with only
people:write gets a trimmed echo — {id, workspace_id, userId} — because
the full response merges the stored record, which is a read. This applies to
Create and Upsert alike.Get a Person
Retrieve a person by their userId — the external identifier you provided when creating them.
Endpoint: GET /api/v1/people/{id}
{id} path parameter is your external userId, not the Flameup id UUID returned in the response body.const response = await fetch(
`https://api.flameup.ai/api/v1/people/${userId}`,
{
headers: {
'Authorization': `Bearer ${API_KEY}`
}
}
);
const person = await response.json();
response = requests.get(
f'https://api.flameup.ai/api/v1/people/{user_id}',
headers={'Authorization': f'Bearer {API_KEY}'}
)
person = response.json()
curl "https://api.flameup.ai/api/v1/people/{userId}" \
-H "Authorization: Bearer your_api_key"
userId currently returns 500, not
404, on GET and PUT (DELETE returns a proper 404). If a GET here starts
answering 500 Internal server error, check the identifier before assuming an
outage. This is a known wart, not a contract to build on.Update a Person
Update an existing person's attributes.
Endpoint: PUT /api/v1/people/{id}
{id} path parameter is your external userId, not the Flameup id UUID.const response = await fetch(
`https://api.flameup.ai/api/v1/people/${userId}`,
{
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${API_KEY}`
},
body: JSON.stringify({
traits: {
plan: 'enterprise', // Update existing
renewal_date: '2025-01-15' // Add new attribute
}
})
}
);
const result = await response.json();
// { "message": "Person updated successfully" }
response = requests.put(
f'https://api.flameup.ai/api/v1/people/{user_id}',
headers={
'Content-Type': 'application/json',
'Authorization': f'Bearer {API_KEY}'
},
json={
'traits': {
'plan': 'enterprise',
'renewal_date': '2025-01-15'
}
}
)
result = response.json() # {'message': 'Person updated successfully'}
curl -X PUT "https://api.flameup.ai/api/v1/people/{userId}" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your_api_key" \
-d '{
"traits": {
"plan": "enterprise",
"renewal_date": "2025-01-15"
}
}'
A successful update returns 200 OK with a confirmation message, not the updated person object:
{
"message": "Person updated successfully"
}
null stores a null value for that key — it does not remove the key.A handful of trait keys are reserved and also update the person's dedicated
fields: email, phone, first_name, last_name, timezone, locale.
Sending traits.email updates the person's email, and an empty string
clears email or phone.
Upsert a Person
Create a person if they don't exist, or update them if they do. This is the recommended approach for most integrations.
Endpoint: POST /api/v1/people/upsert
const response = await fetch(
`https://api.flameup.ai/api/v1/people/upsert`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${API_KEY}`
},
body: JSON.stringify({
userId: 'user_12345',
email: 'jane@example.com',
traits: {
first_name: 'Jane',
last_name: 'Doe',
last_login: new Date().toISOString()
}
})
}
);
const person = await response.json();
response = requests.post(
f'https://api.flameup.ai/api/v1/people/upsert',
headers={
'Content-Type': 'application/json',
'Authorization': f'Bearer {API_KEY}'
},
json={
'userId': 'user_12345',
'email': 'jane@example.com',
'traits': {
'first_name': 'Jane',
'last_name': 'Doe',
'last_login': datetime.utcnow().isoformat()
}
}
)
person = response.json()
curl -X POST "https://api.flameup.ai/api/v1/people/upsert" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your_api_key" \
-d '{
"userId": "user_12345",
"email": "jane@example.com",
"traits": {
"first_name": "Jane",
"last_name": "Doe",
"last_login": "2024-01-20T14:45:00Z"
}
}'
Upsert returns 201 Created whether the person was newly created or updated.
The body is the same as Create a Person: the full merged
person object when your key holds people:read, the trimmed
{id, workspace_id, userId} echo when it does not.
List People
Retrieve a paginated list of people.
Endpoint: GET /api/v1/people
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | integer | 50 | Number of results (max 500) |
offset | integer | 0 | Pagination offset |
const params = new URLSearchParams({
limit: '50',
offset: '0'
});
const response = await fetch(
`https://api.flameup.ai/api/v1/people?${params}`,
{
headers: {
'Authorization': `Bearer ${API_KEY}`
}
}
);
const { people, total, limit, offset } = await response.json();
response = requests.get(
f'https://api.flameup.ai/api/v1/people',
headers={'Authorization': f'Bearer {API_KEY}'},
params={
'limit': 50,
'offset': 0
}
)
data = response.json()
people = data['people']
total = data['total']
curl "https://api.flameup.ai/api/v1/people?limit=50&offset=0" \
-H "Authorization: Bearer your_api_key"
Response:
{
"people": [...],
"total": 1250,
"limit": 50,
"offset": 0
}
Search People
Find people by a search term, optionally filtered by status.
Endpoint: GET /api/v1/people/search
| Parameter | Type | Description |
|---|---|---|
q | string | Search term. search is accepted as an alias; if both are sent, q wins |
status | string | Filter by status, e.g. active |
limit | integer | Default 50, max 500 |
offset | integer | Default 0 |
curl "https://api.flameup.ai/api/v1/people/search?q=jane&limit=10" \
-H "Authorization: Bearer {API_KEY}"
The response is the same shape as List People: people,
total, limit, offset.
q returns the whole workspace, paginated — the same as List People. If you know the exact identifier, Get a Person is cheaper.Batch Upsert
Create or update multiple people in a single request.
Endpoint: POST /api/v1/people/batch
Limits, checked before anything is processed:
- 1,000 operations per request — more returns
400 - 1 MB body — more returns
413 - The body takes an
identifyarray only. Atrackarray returns400pointing you atPOST /api/v1/track/batch— events are a different permission (events:write) and a different route
const response = await fetch(
`https://api.flameup.ai/api/v1/people/batch`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${API_KEY}`
},
body: JSON.stringify({
identify: [
{
userId: 'user_001',
email: 'alice@example.com',
traits: { first_name: 'Alice', plan: 'basic' }
},
{
userId: 'user_002',
email: 'bob@example.com',
traits: { first_name: 'Bob', plan: 'premium' }
}
]
})
}
);
const { results, total } = await response.json();
response = requests.post(
f'https://api.flameup.ai/api/v1/people/batch',
headers={
'Content-Type': 'application/json',
'Authorization': f'Bearer {API_KEY}'
},
json={
'identify': [
{
'userId': 'user_001',
'email': 'alice@example.com',
'traits': {'first_name': 'Alice', 'plan': 'basic'}
},
{
'userId': 'user_002',
'email': 'bob@example.com',
'traits': {'first_name': 'Bob', 'plan': 'premium'}
}
]
}
)
result = response.json()
print(f"Processed {result['total']} records")
curl -X POST "https://api.flameup.ai/api/v1/people/batch" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your_api_key" \
-d '{
"identify": [
{"userId": "user_001", "email": "alice@example.com", "traits": {"first_name": "Alice"}},
{"userId": "user_002", "email": "bob@example.com", "traits": {"first_name": "Bob"}}
]
}'
The response contains a results array with one entry per submitted record, plus a total count:
{
"results": [
{
"type": "identify",
"index": 0,
"success": true,
"data": { "id": "550e8400-e29b-41d4-a716-446655440000", "userId": "user_001" }
},
{
"type": "identify",
"index": 1,
"success": false,
"error": "validation error message"
}
],
"total": 2
}
| Field | Type | Description |
|---|---|---|
type | string | Operation type (identify) |
index | integer | Position of the record in the submitted identify array |
success | boolean | Whether this record was processed successfully |
error | string | Error message (present only when success is false) |
data | object | The resulting person object (present only when success is true) |
Per-item failures come back inside a 200 — check each entry's success, not
the HTTP status. Note one asymmetry: batch data is the full person
object even for a key without people:read; the trimmed-echo gating applies
to the single Create/Upsert routes only.
Delete a Person
Delete a person and all associated data.
Endpoint: DELETE /api/v1/people/{id}
{id} path parameter is your external userId, not the Flameup id UUID.const response = await fetch(
`https://api.flameup.ai/api/v1/people/${userId}`,
{
method: 'DELETE',
headers: {
'Authorization': `Bearer ${API_KEY}`
}
}
);
if (response.ok) {
console.log('Person deleted');
}
response = requests.delete(
f'https://api.flameup.ai/api/v1/people/{user_id}',
headers={'Authorization': f'Bearer {API_KEY}'}
)
if response.status_code == 200:
print('Person deleted')
curl -X DELETE "https://api.flameup.ai/api/v1/people/{userId}" \
-H "Authorization: Bearer your_api_key"
A successful delete returns 200 OK:
{
"message": "Person deleted successfully"
}
Get Person Events
Retrieve events for a specific person. See the Events API for how events are tracked.
Endpoint: GET /api/v1/people/{external_id}/events
{external_id} path parameter is your own user id — the same value you sent to /identify, not the Flameup person UUID. This endpoint requires events:read.Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | integer | 50 | Number of events to return (max 500) |
const response = await fetch(
`https://api.flameup.ai/api/v1/people/${userId}/events?limit=50`,
{
headers: {
'Authorization': `Bearer ${API_KEY}`
}
}
);
const { events, total } = await response.json();
response = requests.get(
f'https://api.flameup.ai/api/v1/people/{user_id}/events',
headers={'Authorization': f'Bearer {API_KEY}'},
params={'limit': 50}
)
data = response.json()
events = data['events']
curl "https://api.flameup.ai/api/v1/people/{userId}/events?limit=50" \
-H "Authorization: Bearer your_api_key"
Response:
{
"success": true,
"person_id": "user_12345",
"events": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"type": "purchase_completed",
"parameters": { "amount": 49.99, "currency": "USD" },
"created_at": "2024-01-20T14:45:00Z"
}
],
"total": 1
}
The payload is wrapped: success is true on any 200, person_id echoes the
{external_id} you asked for (not the Flameup UUID), and total counts the
events in this response — it is the size of the returned page, not the person's
lifetime event count.
Each event object contains the event name in the type field, along with its parameters and created_at timestamp.
Best Practices
The upsert endpoint is ideal for most use cases. It handles both creation and updates in a single call, making your integration simpler and more resilient.