Skip to Content
DocsAPIProfiles

Profiles

Pug maintains a profile for every user your project tracks — identified or anonymous. Profiles accumulate traits (properties) and are linked to the events they generate.

Two RPC services cover the full lifecycle:

ServiceAuthWhat it does
sdk.profiles.v1.ProfilesSDKServicex-api-key (SDK key)Write path — create or update a profile from the SDK
shared.profiles.v1.ProfilesServiceDual (prv_… or JWT)Read / manage — query, list, delete

See Authentication and the auth matrix for key types and headers.

ProfilesSDKService — write path

POST /sdk.profiles.v1.ProfilesSDKService/Identify

Creates or updates a profile identified by external_id. If the profile already exists, traits are shallow-merged — provided keys overwrite stored values for those keys, other keys are left untouched.

Traits are plain JSON, not PropertyValue-wrapped. Unlike event customProperties (which use { "intValue": "5" }-style wrappers), profile traits take bare scalars — see traits type below and the Events API.

Auth: x-api-key header with a public key (pub_…) or private key (prv_…). The project is resolved from the key — no Authorization header or x-project-id is required.

IdentifyRequest fields

Proto fieldJSON nameTypeRequiredNotes
external_idexternalIdstringYesStable user identifier — e.g. your database user ID or email. Must be non-empty
traitstraitsgoogle.protobuf.StructNoPlain JSON object. Shallow-merged into existing traits. On key conflict these values win
anonymous_idanonymousIdstringNoSDK-generated anonymous ID. Must be empty or start with anon-; max 255 characters. Send on first identify to merge prior anonymous activity
device_iddeviceIdstringNoDevice identifier (UUID, max 36 characters) to attach to this profile. Mobile SDKs send it on first identify and on account switch — not on every call. Omit for web

traits type — plain JSON, not PropertyValue

traits is typed as google.protobuf.Struct, which serialises as a plain JSON object in Connect JSON — not the PropertyValue-wrapped format used by event customProperties. You do not need the { "stringValue": "…" } wrapper:

{ "traits": { "email": "[email protected]", "plan": "pro", "seats": 5, "trial": false } }

Compare with events (which use PropertyValue): { "stringValue": "pro" }. Profiles use plain scalars.

anonymous_id constraint

The field must be either empty (omitted) or a string starting with anon-. Any other value returns invalid_argument. The Web SDK generates IDs in this format automatically.

Anonymous → identified merge

When you include anonymous_id on an Identify call, the server merges the anonymous profile into the identified one — attaching prior events and properties — then soft-deletes the anonymous record. Send anonymous_id on the first identify only (when the user signs in). You do not need a separate alias() call; there is no such method.

IdentifyResponse

The response is empty ({}). Writes are asynchronous — the profile record may not be visible via ProfilesService for a few seconds after the call returns.

Wire example

curl -X POST https://api.pugs.dev/sdk.profiles.v1.ProfilesSDKService/Identify \ -H "Content-Type: application/json" \ -H "Connect-Protocol-Version: 1" \ -H "x-api-key: pub_…" \ -d '{ "externalId": "user-123", "anonymousId": "anon-abc456", "traits": { "email": "[email protected]", "plan": "pro", "company": "Acme Inc" } }'

For a full curl and language recipe see HTTP transport — Identifying users.

ProfilesService — read and manage

Auth: Dual — either a private key (prv_…) via x-api-key, or a dashboard JWT via Authorization: Bearer. Public keys are rejected on this path.

x-api-key: prv_… # — or — Authorization: Bearer <jwt>

Methods

MethodEndpoint
GetPOST /shared.profiles.v1.ProfilesService/Get
GetByExternalIdPOST /shared.profiles.v1.ProfilesService/GetByExternalId
ListPOST /shared.profiles.v1.ProfilesService/List
DeletePOST /shared.profiles.v1.ProfilesService/Delete

Get

Fetch a profile by its internal Pug ID.

Request

FieldTypeRequiredNotes
idstringYesInternal profile ID (as returned by List or GetByExternalId)
{ "id": "01966e2a-e4b2-7000-9000-000000000001" }

Response — returns a Profile object.

{ "profile": { "id": "01966e2a-e4b2-7000-9000-000000000001", "externalId": "user-123", "properties": { "email": "[email protected]", "plan": "pro" }, "projectId": "proj_abc", "createTime": "2026-01-15T10:00:00Z", "updateTime": "2026-06-01T14:30:00Z", "activity": { "firstSeen": "2026-01-15T10:00:00Z", "lastSeen": "2026-06-01T14:30:00Z", "totalEvents": 342, "pageviews": 198, "sessions": 27, "browser": "Chrome", "os": "macOS", "country": "US" } } }

GetByExternalId

Fetch a profile by the external_id you passed to Identify.

Request

FieldTypeRequiredNotes
external_idstringYesThe stable user identifier set on Identify
{ "externalId": "user-123" }

Response — same shape as Get.

List

Search and page through profiles. List is server-streaming — the server may return results in multiple response frames over a single HTTP/2 stream (or chunked HTTP/1.1). Each frame carries a batch of profiles and a next_page_token.

Request

FieldJSON nameTypeNotes
page_tokenpageTokenstringOpaque cursor from a previous response’s nextPageToken. Empty (or omitted) for the first page
filter_groupsfilterGroupsFilterGroup[]Zero or more filter groups. Each group has filters (array of PropertyFilter) and an operator (LOGICAL_OPERATOR_AND or LOGICAL_OPERATOR_OR) to combine within the group
filter_groups_operatorfilterGroupsOperatorLogicalOperatorHow to combine multiple filterGroupsLOGICAL_OPERATOR_AND or LOGICAL_OPERATOR_OR. Defaults to LOGICAL_OPERATOR_AND when unspecified

Each PropertyFilter has three fields: property (the trait key, e.g. "plan"), operator (full enum form — FILTER_OPERATOR_EQUALS, FILTER_OPERATOR_NOT_EQUALS, FILTER_OPERATOR_CONTAINS, FILTER_OPERATOR_NOT_CONTAINS, FILTER_OPERATOR_IS_SET, FILTER_OPERATOR_IS_NOT_SET, FILTER_OPERATOR_LTE, FILTER_OPERATOR_GTE, FILTER_OPERATOR_LT, FILTER_OPERATOR_GT, FILTER_OPERATOR_IN, FILTER_OPERATOR_NOT_IN, FILTER_OPERATOR_BETWEEN, FILTER_OPERATOR_NOT_BETWEEN; see Insights → Filter operators), and value/values (string or string array for multi-value operators).

Example — filter by plan

{ "filterGroups": [ { "filters": [ { "property": "plan", "operator": "FILTER_OPERATOR_EQUALS", "value": "pro" } ], "operator": "LOGICAL_OPERATOR_AND" } ] }

Response (streamed)

{ "profiles": [ { "id": "…", "externalId": "user-123", "properties": { "plan": "pro" }, "…": "…" } ], "nextPageToken": "eyJvZmZzZXQiOjUwfQ" }

Pass nextPageToken as pageToken in your next request. An empty nextPageToken means no more pages.

Delete

Soft-deletes a profile by its internal ID. The profile is hidden from List and GetByExternalId but event data in ClickHouse is retained.

Request

FieldTypeRequiredNotes
idstringYesInternal profile ID
{ "id": "01966e2a-e4b2-7000-9000-000000000001" }

Response — empty ({}).

Profile message

Returned by Get, GetByExternalId, and each frame of List.

Proto fieldJSON nameTypeNotes
ididstringInternal Pug profile ID
external_idexternalIdstringStable user identifier set via Identify
propertiespropertiesgoogle.protobuf.StructTrait map — plain JSON object (same encoding as IdentifyRequest.traits)
project_idprojectIdstringProject this profile belongs to
create_timecreateTimeRFC 3339 timestampWhen the profile was first created
update_timeupdateTimeRFC 3339 timestampWhen traits were last updated
activityactivityProfileActivitySummaryAggregated activity data (see below)

ProfileActivitySummary fields

Proto fieldJSON nameTypeNotes
first_seenfirstSeenRFC 3339 timestampEarliest event timestamp for this profile
last_seenlastSeenRFC 3339 timestampMost recent event timestamp
total_eventstotalEventsint64All events attributed to this profile
pageviewspageviewsint64Page-view event count
sessionssessionsint64Session count
browserbrowserstringMost recently seen browser name
browser_versionbrowserVersionstringBrowser version
ososstringOperating system name
os_versionosVersionstringOS version
devicedevicestringDevice type or model
countrycountrystringISO 3166-1 alpha-2 country code
regionregionstringRegion / state
citycitystringCity name

Activity fields are derived from ClickHouse event data. They reflect writes with a short lag — typically a few seconds after Identify returns.

Errors

CodeHTTPCause
unauthenticated401Missing, malformed, or expired credential
permission_denied403Public key used on a shared.* endpoint
invalid_argument400external_id empty; anonymous_id does not match ^$|^anon- or exceeds 255 characters; device_id exceeds 36 characters
not_found404No profile with the given id or external_id
internal500Server error — retry with exponential backoff

Further reading

Last updated on