Identity
Identity endpoints — claim anonymous events for a known user ID, set user properties, and submit attribution tokens.
Identity management: link anonymous events to a known user ID after authentication, attach key-value properties to users, and submit ad-network attribution tokens.
POST /v1/identity/claim
Reassign all events, metric events, and funnel events from an anonymous user to a known user ID. Also merges the corresponding user records.
Auth required: Yes (client API key with events:write permission)
Request
{
"anonymous_id": "owl_anon_abc123xyz",
"user_id": "user-456"
}| Field | Type | Required | Description |
|---|---|---|---|
anonymous_id | string | Yes | The anonymous ID to claim. Must start with owl_anon_. |
user_id | string | Yes | The authenticated user's ID. Must not start with owl_anon_. |
Validation rules
anonymous_idmust start with theowl_anon_prefix.user_idmust not start withowl_anon_.- The API key must be scoped to an app (client keys only).
Behavior
- All
eventsrows whereuser_idmatchesanonymous_idacross all apps in the project are updated to the newuser_id. - All
funnel_eventsandmetric_eventsrows with the same anonymous user_id are also reassigned across all project apps. - The user record for the anonymous ID is merged into the identified user's record. The
claimed_fromarray on the identified user is updated to include the anonymous ID,first_seen_attakes the earlier of the two records, and app associations are merged. - The anonymous user record is deleted.
Idempotent
If the same anonymous_id has already been claimed by the same user_id, the endpoint returns success with events_reassigned_count: 0 without making any changes.
Response
// Success (200)
{
"claimed": true,
"events_reassigned_count": 42
}
// Already claimed (200)
{
"claimed": true,
"events_reassigned_count": 0
}Returns 404 if no events exist for the given anonymous_id across the project's apps.
SDK integration note
SDKs must flush all buffered events before calling this endpoint. Otherwise, in-flight events sent after the claim may still carry the old anonymous ID and will not be reassigned.
Ingest-side re-attribution
Once an anonymous ID has been claimed, /v1/ingest also re-attributes any events that arrive tagged with that anon ID to the real user. This keeps offline-queued events flushed after the claim consistent with events sent before it — you do not need a second claim call to mop up stragglers.
POST /v1/identity/properties
Set, merge, or delete user properties for a known user. Properties are stored as a JSONB object on the app_users table and are project-scoped.
Auth required: Yes (client API key with users:write permission)
Request
{
"user_id": "user-456",
"properties": {
"plan": "pro",
"signup_source": "landing_page",
"removed_key": ""
}
}| Field | Type | Required | Description |
|---|---|---|---|
user_id | string | Yes | The user to update. May be an anonymous ID (owl_anon_*) or a real user ID. |
properties | object | Yes | Key-value string pairs to merge. Empty-string values delete the key. |
Validation rules
- Keys must be non-empty strings, max 50 characters.
- Values must be strings, max 200 characters.
- After merging, total property count cannot exceed 50.
- The API key must be scoped to an app (client keys only).
- Value of
""(empty string) deletes the key from the user's properties.
Response
// Success (200)
{
"updated": true,
"properties": {
"plan": "pro",
"signup_source": "landing_page"
}
}The returned properties object reflects the full post-merge state for the user. The merge operation is race-condition safe.
POST /v1/identity/attribution/:source
Submit an ad-network attribution token for server-side resolution. The server calls the network's attribution API (e.g. Apple) and writes the result to the user's properties via the same merge semantics as /v1/identity/properties.
Auth required: Yes (client API key with users:write permission, scoped to an app)
Path parameters
| Parameter | Values | Description |
|---|---|---|
source | apple-search-ads | Attribution network. The route is future-proofed — additional networks will slot in under new source values. |
Request
{
"user_id": "owl_anon_A1B2…",
"attribution_token": "<AdServices token>"
}| Field | Type | Required | Description |
|---|---|---|---|
user_id | string | Yes | The user (anonymous or real) to attach attribution properties to. |
attribution_token | string | Yes | Opaque token from the ad network. For Apple Search Ads this is AAAttribution.attributionToken(). |
dev_mock | "attributed" | "unattributed" | "pending" | No | Development helper — ignored when NODE_ENV=production. |
Response
// Attributed (200)
{
"attributed": true,
"pending": false,
"properties": {
"attribution_source": "apple_search_ads",
"asa_campaign_id": "123",
"asa_ad_group_id": "456",
"asa_keyword_id": "789",
"asa_claim_type": "click",
"asa_ad_id": "42",
"asa_creative_set_id": "7"
}
}
// Not attributed (200)
{ "attributed": false, "pending": false, "properties": { "attribution_source": "none" } }
// Apple test fixture — TestFlight, Xcode dev build, or simulator (200)
{ "attributed": false, "pending": false, "properties": { "attribution_source": "apple_test_install" } }
// Pending — Apple hasn't built the record yet (200)
{ "attributed": null, "pending": true, "retry_after_seconds": 3600, "properties": {} }On a pending response the caller should retry after retry_after_seconds. The Swift SDK retries across app launches up to ASA_MAX_PENDING_ATTEMPTS (5) times before writing attribution_source = "none" and stopping. Returns 400 for an invalid token, 502 for an upstream Apple error.
Properties written
The server writes via the same mergeUserProperties helper used by /v1/identity/properties, so these keys merge into the user's existing properties and survive identity claim:
attribution_source—"apple_search_ads"when Apple attributed,"none"when capture completed but nothing attributed, or"apple_test_install"when Apple returned its deliberate non-production fixture (TestFlight, Xcode-deployed dev build on a real device, or the iOS simulator). The fixture is detected by the structural tell of a single numeric ID repeated across campaign, ad group, and ad — something real Apple data can never produce.asa_campaign_id,asa_ad_group_id,asa_keyword_id,asa_claim_type,asa_ad_id,asa_creative_set_id— populated only whenattributedistrue. Not written forapple_test_installresponses: the IDs Apple returns in that case are placeholders, so storing them just pollutes dashboards and burns an Apple Ads enrichment call.
