Skip to content

Profiles API

Identify users, read and list profiles, and run data-subject deletion across the SDK and shared profile services.

Updated View as Markdown

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:

Service Auth What it does
sdk.profiles.v1.ProfilesSDKService x-api-key (SDK key) Write path — create or update a profile from the SDK
shared.profiles.v1.ProfilesService Dual (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 field JSON name Type Required Notes
external_id externalId string Yes Stable user identifier — e.g. your database user ID or email. Must be non-empty
traits traits google.protobuf.Struct No Plain JSON object. Shallow-merged into existing traits. On key conflict these values win
anonymous_id anonymousId string No SDK-generated anonymous ID. Must be empty or start with anon-; max 255 characters. Send on first identify to merge prior anonymous activity
device_id deviceId string No Device 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

Method Endpoint
Get POST /shared.profiles.v1.ProfilesService/Get
GetByExternalId POST /shared.profiles.v1.ProfilesService/GetByExternalId
List POST /shared.profiles.v1.ProfilesService/List
Delete POST /shared.profiles.v1.ProfilesService/Delete

Get

Fetch a profile by its internal Pug ID.

Request

Field Type Required Notes
id string Yes Internal 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

Field Type Required Notes
external_id string Yes The 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

Field JSON name Type Notes
page_token pageToken string Opaque cursor from a previous response’s nextPageToken. Empty (or omitted) for the first page
filter_groups filterGroups FilterGroup[] 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_operator filterGroupsOperator LogicalOperator How 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

Field Type Required Notes
id string Yes Internal profile ID
{ "id": "01966e2a-e4b2-7000-9000-000000000001" }

Response — empty ({}).

Profile message

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

Proto field JSON name Type Notes
id id string Internal Pug profile ID
external_id externalId string Stable user identifier set via Identify
properties properties google.protobuf.Struct Trait map — plain JSON object (same encoding as IdentifyRequest.traits)
project_id projectId string Project this profile belongs to
create_time createTime RFC 3339 timestamp When the profile was first created
update_time updateTime RFC 3339 timestamp When traits were last updated
activity activity ProfileActivitySummary Aggregated activity data (see below)

ProfileActivitySummary fields

Proto field JSON name Type Notes
first_seen firstSeen RFC 3339 timestamp Earliest event timestamp for this profile
last_seen lastSeen RFC 3339 timestamp Most recent event timestamp
total_events totalEvents int64 All events attributed to this profile
pageviews pageviews int64 Page-view event count
sessions sessions int64 Session count
browser browser string Most recently seen browser name
browser_version browserVersion string Browser version
os os string Operating system name
os_version osVersion string OS version
device device string Device type or model
country country string ISO 3166-1 alpha-2 country code
region region string Region / state
city city string City name

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

Errors

Code HTTP Cause
unauthenticated 401 Missing, malformed, or expired credential
permission_denied 403 Public key used on a shared.* endpoint
invalid_argument 400 external_id empty; anonymous_id does not match ^$|^anon- or exceeds 255 characters; device_id exceeds 36 characters
not_found 404 No profile with the given id or external_id
internal 500 Server error — retry with exponential backoff

Further reading

Navigation

Type to search…

↑↓ navigate↵ selectEsc close