Profiles API
User identity and trait management. Writes are async; reads come from ClickHouse.
Services: ProfilesSDKService (SDK auth), ProfilesService (shared auth)
ProfilesSDKService — Identify
Link an anonymous profile to an external ID and merge traits. Called by SDKs via identify().
Auth: SDK API key
Request
{
"externalId": "user-123",
"traits": {
"email": "user@example.com",
"plan": "pro",
"company": "Acme Inc"
}
}Response
{
"profileId": "prof_abc123"
}TypeScript client
import { ProfilesSDKService } from './gen/profiles/v1/profiles_sdk_connect'
const client = createClient(ProfilesSDKService, transport)
await client.identify({
externalId: 'user-123',
traits: {
email: 'user@example.com',
plan: 'pro'
}
})Behavior
- Creates a new profile if
externalIdis unknown - Merges traits if profile already exists (does not remove existing traits)
- Merges all prior anonymous events into the identified profile
- Write is async — profile may not appear in search for a few seconds
ProfilesSDKService — Alias
Merge two profile IDs into one. Use when a user had separate anonymous profiles before and after signup, or when migrating from another system.
Auth: SDK API key
{
"previousId": "anonymous-abc",
"externalId": "user-123"
}All events from previousId are attributed to the profile for externalId.
ProfilesService — Read and manage
Server-side profile operations. Requires shared API key or JWT.
Auth: Shared API key or JWT
Get profile
// Request
{ "externalId": "user-123" }
// Response
{
"profile": {
"id": "prof_abc123",
"externalId": "user-123",
"traits": {
"email": "user@example.com",
"plan": "pro"
},
"firstSeen": "2026-01-15T10:00:00Z",
"lastSeen": "2026-06-01T14:30:00Z",
"eventCount": 342
}
}List profiles
// Request
{
"query": "user@example.com",
"limit": 50,
"offset": 0
}
// Response
{
"profiles": [ ... ],
"total": 1
}Search by external ID, email trait, or custom trait filters.
Delete profile
Soft-delete a profile. Events remain in ClickHouse but the profile is hidden from search and marked deleted in Postgres.
// Request
{ "externalId": "user-123" }Storage model
Identify / Alias (SDK)
→ NATS profile worker
→ Postgres (profile record, traits)
→ ClickHouse (event attribution update)
Read (ProfilesService)
→ ClickHouse (event history, derived traits)
→ Postgres (soft-delete state, trait overrides)Read lag: Profile writes are async. After identify(), wait 2–5 seconds before querying via ProfilesService.Get.
SDK integration
Client-side identity is handled by the SDK — you rarely call ProfilesSDKService directly:
import { identify, reset } from '@poluruprvn/pug-web'
identify('user-123', { email: 'user@example.com' })
reset() // on sign-outSee Identity & sessions.
Dashboard
Profile search and detail UI: Profiles
Errors
| Error | Cause | Fix |
|---|---|---|
not_found | External ID doesn’t exist | Confirm identify was called |
invalid_argument | Empty external ID | Provide non-empty string |
| Profile not in search after identify | Async write lag | Wait a few seconds and retry |
Further reading
- Identity & sessions
- Profiles dashboard
- Retention analysis — requires consistent identity