Owlmetry
API Reference

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"
}
FieldTypeRequiredDescription
anonymous_idstringYesThe anonymous ID to claim. Must start with owl_anon_.
user_idstringYesThe authenticated user's ID. Must not start with owl_anon_.

Validation rules

  • anonymous_id must start with the owl_anon_ prefix.
  • user_id must not start with owl_anon_.
  • The API key must be scoped to an app (client keys only).

Behavior

  1. All events rows where user_id matches anonymous_id across all apps in the project are updated to the new user_id.
  2. All funnel_events and metric_events rows with the same anonymous user_id are also reassigned across all project apps.
  3. The user record for the anonymous ID is merged into the identified user's record. The claimed_from array on the identified user is updated to include the anonymous ID, first_seen_at takes the earlier of the two records, and app associations are merged.
  4. 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": ""
  }
}
FieldTypeRequiredDescription
user_idstringYesThe user to update. May be an anonymous ID (owl_anon_*) or a real user ID.
propertiesobjectYesKey-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

ParameterValuesDescription
sourceapple-search-adsAttribution 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>"
}
FieldTypeRequiredDescription
user_idstringYesThe user (anonymous or real) to attach attribution properties to.
attribution_tokenstringYesOpaque token from the ad network. For Apple Search Ads this is AAAttribution.attributionToken().
dev_mock"attributed" | "unattributed" | "pending"NoDevelopment 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 when attributed is true. Not written for apple_test_install responses: the IDs Apple returns in that case are placeholders, so storing them just pollutes dashboards and burns an Apple Ads enrichment call.

Ready to get started?

Connect your agent via MCP or CLI and start tracking.