Core APIsPeople

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.

Required Permission: 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

FieldTypeDescription
idUUIDFlameup's internal identifier (read-only)
workspace_idstringWorkspace identifier (read-only)
userIdstringYour unique identifier for this user (required, max 255 chars)
emailstringUser's email address (optional, max 254 chars)
phonestringUser's phone number (optional, max 50 chars)
anonymousIdstringAnonymous identifier for merging anonymous activity (transient, not persisted)
traitsobjectCustom 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
statusstringConsent state — active, unsubscribed, bounced, or suppressed. Always present on reads (read-only here)
unsubscribed_attimestampWhen the person unsubscribed. Omitted unless set (read-only)
unsubscribe_reasonstringWhy the person unsubscribed, max 255 chars. Omitted unless set (read-only)
created_attimestampWhen the person was created (read-only)
updated_attimestampWhen 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.
Trait keys are validated, and six are reserved. A key must start with a letter and contain only letters, digits and underscores (^[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();
Use a stable, unique identifier from your system as the userId. Database primary keys or UUIDs work well.
The identifier can be sent as userId, user_id, or external_id — these are aliases for the same value.
What the response echoes depends on 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}

The {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();
An unknown 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}

The {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" }

A successful update returns 200 OK with a confirmation message, not the updated person object:

{
  "message": "Person updated successfully"
}
Updates are merged with existing trait data. Setting a trait to 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();

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

ParameterTypeDefaultDescription
limitinteger50Number of results (max 500)
offsetinteger0Pagination 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:

{
  "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

ParameterTypeDescription
qstringSearch term. search is accepted as an alias; if both are sent, q wins
statusstringFilter by status, e.g. active
limitintegerDefault 50, max 500
offsetintegerDefault 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.

Omitting 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 identify array only. A track array returns 400 pointing you at POST /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();

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
}
FieldTypeDescription
typestringOperation type (identify)
indexintegerPosition of the record in the submitted identify array
successbooleanWhether this record was processed successfully
errorstringError message (present only when success is false)
dataobjectThe 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}

The {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');
}

A successful delete returns 200 OK:

{
  "message": "Person deleted successfully"
}
This action is irreversible. The person's events and device tokens are deleted with them. One caveat: event copies in the analytics store (ClickHouse) are not removed — aggregate analytics computed before the delete keep counting them.

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

The {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

ParameterTypeDefaultDescription
limitinteger50Number 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:

{
  "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.

Workspace-level people statistics are available in the Flameup dashboard.

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.