Skip to Content

Profile API

The Profile API reads one Store Entry by key and returns its attributes as JSON. It is a serve-by-key surface: there is no list, search, or query endpoint — you must know the key you are asking about. It is a server-side surface: what it returns is one person’s attributes, and the credential that reads them is a secret.

Available in workspaces with Profile enabled. A platform administrator enables Profile per workspace, and the serving key this API requires can only be created in an enabled workspace. See Profile for how to get access.

Endpoint

GET /v1/stores/{store}/entries/id/{value}
ParameterRequiredDescription
storeYesThe Store name, as created in the app.
id—A fixed path segment. Entries are addressed by their primary index; no other index is served, and any other value here returns 400.
valueYesThe key value to look up — the primary key of the row the feed published.

The base URL is the Profile API host for your deployment — https://profiles.zeotap.com by default. The exact URL for your workspace, with a copyable request, is shown on Personalize → Profile → API.

Query parameters

ParameterDescription
fieldsA comma-separated subset of fields to return, for trimming a wide profile to what a page actually renders. Naming a field the Entry does not have is not an error; it is simply absent. The subset is applied after the read, so it saves bytes on the wire rather than lookup work.
includeComma-separated extras to fold into the response. The one supported value is audiences — see below. Anything else returns 400.

The two are independent: ?fields=tier&include=audiences returns tier and the audience list.

Authentication

Pass a serving key as a Bearer token. Serving keys carry a svk_ prefix and the serve:profile scope, which is the only scope this surface accepts. Keys are workspace-level, so one key reads any store in the workspace, and the workspace is taken from the key — there is no workspace header. Create keys on Personalize → Profile → API keys.

A serving key is a secret. It is hashed at rest, shown in full exactly once at creation, and may be given an expiry; after that only its prefix is listed, so store it in a secrets manager or an environment variable when you create it. Call this endpoint from your own backend, never from a browser or a mobile binary — a key that reads any store in the workspace should not be shipped to a device. There is no permissive CORS on this endpoint for the same reason.

curl -H 'Authorization: Bearer <SERVING_KEY>' \ 'https://profiles.zeotap.com/v1/stores/user_profiles/entries/id/user-123'
// Server-side only — this key must not reach a browser. const res = await fetch( "https://profiles.zeotap.com/v1/stores/user_profiles/entries/id/user-123", { headers: { Authorization: "Bearer <SERVING_KEY>" } }, ); const attributes = await res.json();

Platform credentials are refused here, whatever scopes they carry: an sk_ MCP key, an rk_ REST key, and the Membership API’s public pk_ key each answer 401. The endpoint checks the class of the credential before it checks anything else, so widening an MCP key’s scopes cannot make it read a profile. read:store still exists and is still mintable, but it now gates the store MCP tools only and authorises no HTTP read.

If a call that worked before started answering 401, this is why. The serving key replaced the sk_ key on this endpoint outright, with no dual-accept period — mint a svk_ key on Personalize → Profile → API keys and swap it in.

Response

A 200 OK returns the Entry’s merged fields as a JSON object. Where several feeds compose one Entry, the response is the union of every field they own:

{ "plan": "pro", "tier": "gold", "loyalty_points": 4200, "first_name": "Alex" }
StatusMeaning
200 OKEntry found; the body is the merged attributes.
400 Bad RequestA path segment is missing, or the index segment is something other than id.
401 UnauthorizedMissing or malformed Authorization header, an invalid or expired serving key, or a credential of the wrong class — an sk_, rk_, or pk_ key.
403 ForbiddenThe serving key does not carry the serve:profile scope.
404 Not FoundNo such Store in the key’s workspace, or no Entry for the given key — it was never populated, or every field was cleared.
429 Too Many RequestsRate limit exceeded; retry after the Retry-After header value, in seconds.

A 404 is a normal answer, not an error to alert on. It means the key has no published attributes — a person your feeds have never seen, or one whose source row has since been removed. Treat it as “no personalization available” and fall back to your default experience.

Member audiences

A page that personalizes usually needs two things about a person: their attributes, and which audiences they are in. Ask for both in one request:

GET /v1/stores/{store}/entries/id/{value}?include=audiences
{ "plan": "pro", "tier": "gold", "loyalty_points": 4200, "_audiences": ["aud_cart_abandoners", "aud_vip"] }

_audiences lists the realtime audiences this person is in, by id, and only the ones they are in. Your existing serving key already authorises it — there is no extra scope and no new key to mint. It is opt-in because it costs a lookup per audience: a read that does not ask pays nothing for it and gets exactly the response it always got.

An absent _audiences is not an empty one.

"_audiences": [] means the lookup ran and this person is in no audiences. The key being missing means the read could not answer for membership at all — and served you the attributes anyway, rather than failing the whole request over an optional part of it.

Check that the key is present before you treat someone as a non-member. Personalizing on a missing key as though it were [] turns a brief outage into confidently showing the wrong experience.

The key is absent when the realtime plane is unreachable, when the lookup exceeds its deadline, or when your workspace does not have realtime audiences enabled. Your operators can see which on the service’s health endpoint.

How a person is matched

Membership is looked up by the same value you read the Entry by. That is the right value when your Realtime Events map the same column into their user identifier as your feed uses for its primary key — a Store keyed on customer_id and a Realtime Event whose user identifier is customer_id describe one person with one string, and any other identifiers they are known by are resolved for you.

If the two are configured differently — a feed keyed on one column, events carrying another as their user identifier — the lookup answers for a different identifier. The two are declared separately and are not checked against each other, so it is worth confirming they agree.

What it does not return

Audiences you are not in are omitted rather than listed as false, and audiences are returned by id rather than by name. For a definitive yes/no on one named audience, or for a check from a browser, use the Membership API.

Freshness

Attributes are as fresh as the feed that published them. A feed on a 1-hour schedule serves values up to an hour old; a manual feed serves whatever its last refresh wrote. If you need a value to be live, publish it from a model that is itself refreshed at that cadence — the Profile API does not reach back to the warehouse at request time.

_audiences is different, and that difference is the point: membership is recomputed as you ask, from the events received so far, rather than read from anything a feed published. A person who qualified a second ago is in the list.

Next Steps

  • Playground — run this request against a real Store without writing code.
  • Stores — create the Store and feeds this endpoint reads from.
  • Membership API — membership on its own: a definitive yes/no for one named audience, and the one personalization read a browser may make.
Last updated on