Server API reference
The routes your backend calls with a site's secret key, and every shape they take and return. Every route is under /v1/ on the platform's API address. Request and response bodies are JSON.
This page is made from the platform's API document, so it always matches the code. The rules behind the routes (versions, visibility, the owner, freshness) are in contracts/profile-sync.md, and the profile lifecycle guide walks through them with real requests.
openapi.json, beside this page, holds the same routes and shapes as an OpenAPI 3.1 document, for tools such as a client generator.
One site's profiles, which the partner's backend sends with the site's secret key: an upsert on every change, a delete when a profile goes, batches for bulk changes, and a paged list to reconcile, such as once a day. The rules are in contracts/profile-sync.md.
Authentication
secret_key: A site's secret key, sk_..., which the partner console makes and rotates under Keys and shows in full once. Send it with every request as Authorization: Bearer sk_..., over HTTPS, from the partner's servers only: never from a browser or an app, and never into a log. A key reaches only its own site's profiles, so a profile on another site is not found, even when the partner owns that site too. After a rotation, the replaced key keeps working for 24 hours, unless the console ends it sooner. A missing key, or one that does not work, gets 401 (contracts/profile-sync.md, "Authentication").
Operations
| Route | What it does |
|---|---|
| GET /v1/profiles | Lists the profiles the platform holds. |
| POST /v1/profiles/batch | Upserts and deletes profiles in a batch. |
| GET /v1/profiles/{external_id} | Shows the platform's copy of a profile. |
| PUT /v1/profiles/{external_id} | Upserts a profile. |
| DELETE /v1/profiles/{external_id} | Deletes a profile. |
GET /v1/profiles
Lists the profiles the platform holds.
Lists every profile the platform holds for the site, deleted ones included, with the version of its last upsert or delete. Use it to reconcile, regularly, such as once a day, and never once per page view: compare the list with the site's own, by external id, and send again what differs. Upsert each profile the platform does not list or holds at a lower version than the site's. Delete, with a higher version, each profile the platform holds that the site no longer lists. Send those changes in batches (contracts/profile-sync.md, "Reconciliation").
A page holds up to 1,000 profiles, in byte order of external_id as ASCII sorts it: - and ., then digits, then A to Z, then _, then a to z. Pass a page's next as after to get the next page; the last page has no next. The site's database may sort the same ids in another order, so compare the two lists as sets rather than side by side.
Reconcile even when nothing changed. The AI assistant answers from the site's profiles only while a counted request arrived within the last 7 days (or another period the instance's operator set), and each page answered 200 counts (contracts/profile-sync.md, "Keeping the copy fresh").
Send the site's secret key as Authorization: Bearer sk_....
Authentication: secret_key (see Authentication).
Parameters:
| Name | In | Type | Required | Description |
|---|---|---|---|---|
after |
query | string | No | The previous page's next: the last external id on it. Leave it out for the first page. Text that is not an external id gets 400. Limits: 1 to 128 characters; matches ^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$. |
limit |
query | integer | No | The most profiles the page may hold, from 1 to 1,000; 1,000 when left out. A limit outside that range gets 400. Limits: 1 to 1,000. |
Responses:
| Status | When | Body |
|---|---|---|
| 200 | One page of profiles and their versions. | application/json: ProfileVersionPage |
| 400 | The query cannot be read: an after that is no external id, or a limit that is not a whole number from 1 to 1,000. |
application/problem+json: Problem |
| 401 | The secret key is missing or does not work, such as an unknown key, one ended in the partner console, or one a rotation replaced more than 24 hours ago. | application/problem+json: Problem |
| 429 | Too many requests from this network address, or for this site: a site may send about 10 a second, with all its keys together. Ask for the same page again after Retry-After seconds. |
application/problem+json: Problem |
| 500 | A fault on the platform's side. Ask for the same page again later. | application/problem+json: Problem |
| 503 | The platform could not answer, for example because the request took longer than 10 seconds or the instance was switching to another server. Ask for the same page again later. | application/problem+json: Problem |
Header on 429: Retry-After, integer. How many seconds to wait before sending the request again: a whole number, at least 1.
Header on 503: Retry-After, integer. Sent only while the instance is switching to another server: how many seconds to wait before sending the request again.
Example response, 200:
{
"next": "marco",
"profiles": [
{
"deleted": false,
"external_id": "anna",
"version": 42
},
{
"deleted": true,
"external_id": "ines",
"version": 17
},
{
"deleted": false,
"external_id": "marco",
"version": 7
}
]
}
POST /v1/profiles/batch
Upserts and deletes profiles in a batch.
Sends up to 100 upserts and deletes in one request. Use it for bulk changes, such as an initial import or what a reconciliation found, rather than one call per profile. A site may send about 10 requests a second, so 10 batches of 100 a second sync 100,000 profiles in under two minutes.
Each item is decided on its own, as if sent alone with PUT or DELETE, and gets one result, in the items' order. A rejected item does not stop the others or undo them, and items that name the same profile apply in the order sent. The batch is saved as one: the changes it applies take effect together, or none of them does.
Safe to repeat: after a timeout or a 5xx, send the whole batch again. Usually none of it applied. If it was saved just before the failure, the items it applied come back unchanged, and the others as they did before. No idempotency key is needed.
Send total, how many profiles the site lists in all, during an initial import and whenever it changes: the partner console shows "imported of total". A batch answered 200 also keeps the site's profiles fresh for the AI assistant (contracts/profile-sync.md, "Keeping the copy fresh").
Send the site's secret key as Authorization: Bearer sk_....
Authentication: secret_key (see Authentication).
Request body (required): application/json: ProfileBatch. The items, and optionally the site's total.
Example request:
{
"items": [
{
"action": "upsert",
"external_id": "anna",
"fields": {
"assistant_name": {
"value": "Anna",
"visibility": "public"
},
"headline": {
"value": "Classic and sports massage, Baixa",
"visibility": "public"
},
"name": {
"value": "Anna, massage",
"visibility": "public"
}
},
"owner": "sg-owner-anna",
"version": 42
},
{
"action": "upsert",
"external_id": "marco",
"fields": {
"headline": {
"value": "Thai massage, Estrela",
"visibility": "public"
},
"name": {
"value": "Marco Ferreira",
"visibility": "public"
},
"services": {
"value": "Thai massage on a floor mat, 60 or 90 minutes.",
"visibility": "ai_only"
}
},
"owner": "sg-owner-marco",
"version": 7
},
{
"action": "delete",
"external_id": "ines",
"version": 17
}
],
"total": 48
}
Responses:
| Status | When | Body |
|---|---|---|
| 200 | What each item did, one result for each item in the items' order. | application/json: ProfileBatchResults |
| 400 | The request cannot be read, such as a body that is not valid JSON. None of the batch applies. | application/problem+json: Problem |
| 401 | The secret key is missing or does not work, such as an unknown key, one ended in the partner console, or one a rotation replaced more than 24 hours ago. None of the batch applies. | application/problem+json: Problem |
| 413 | The body is larger than 1 MiB (1,048,576 bytes). None of the batch applies: send fewer items in each batch. | application/problem+json: Problem |
| 415 | The body was not sent as JSON. Send it with Content-Type: application/json. None of the batch applies. |
application/problem+json: Problem |
| 422 | The batch cannot be read (about:blank): no items or more than 100, an item with an unknown action, an external_id that breaks its rule or is batch, a version missing or out of range, a field of the wrong type, or a total above 9,007,199,254,740,991. None of the batch applies, and the console lists nothing: fix the batch before sending it again. |
application/problem+json: Problem |
| 429 | Too many requests from this network address, or for this site: a site may send about 10 a second, with all its keys together. None of the batch applies. Send the same batch again after Retry-After seconds. |
application/problem+json: Problem |
| 500 | A fault on the platform's side. Send the whole batch again later: usually none of it applied, and the versions make the repeat safe. | application/problem+json: Problem |
| 503 | The platform could not finish the request, for example because another request was writing one of the same profiles, the request took longer than 10 seconds, or the instance was switching to another server. Send the whole batch again later: usually none of it applied, and the versions make the repeat safe. | application/problem+json: Problem |
Header on 429: Retry-After, integer. How many seconds to wait before sending the request again: a whole number, at least 1.
Header on 503: Retry-After, integer. Sent only while the instance is switching to another server: how many seconds to wait before sending the request again.
Example response, 200:
{
"results": [
{
"external_id": "anna",
"outcome": "applied",
"version": 42
},
{
"external_id": "marco",
"outcome": "unchanged",
"version": 7
},
{
"external_id": "ines",
"outcome": "rejected",
"reason": "version-reused"
}
]
}
GET /v1/profiles/{external_id}
Shows the platform's copy of a profile.
Shows what the platform holds for one of the site's profiles, so the site can check what arrived: the version, when it arrived, the public and AI-only fields with their values, and the names of the private ones. Never a private value, and never the owner. It is for debugging: a sync never needs it, and reading it does not keep the site's profiles fresh for the AI assistant.
A deleted profile shows deleted: true and no fields. A profile the platform never had for the key's site gets 404.
Send the site's secret key as Authorization: Bearer sk_....
Authentication: secret_key (see Authentication).
Parameters:
| Name | In | Type | Required | Description |
|---|---|---|---|---|
external_id |
path | string | Yes | The site's own id for the profile, such as anna: 1 to 128 ASCII letters, digits, -, _ and ., starting with a letter or a digit. Case matters. Never batch, which the batch route's path takes. Limits: 1 to 128 characters; matches ^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$. |
Responses:
| Status | When | Body |
|---|---|---|
| 200 | The platform's copy of the profile. | application/json: ProfileCopy |
| 400 | The path cannot be read, such as an external_id that is not valid UTF-8 once percent-decoded. |
application/problem+json: Problem |
| 401 | The secret key is missing or does not work, such as an unknown key, one ended in the partner console, or one a rotation replaced more than 24 hours ago. | application/problem+json: Problem |
| 404 | The platform never had a profile with this id for the key's site. A profile on another site is not found either, even when the partner owns that site too. A deleted profile is found, with deleted: true. |
application/problem+json: Problem |
| 429 | Too many requests from this network address, or for this site: a site may send about 10 a second, with all its keys together. Ask again after Retry-After seconds. |
application/problem+json: Problem |
| 500 | A fault on the platform's side. Ask again later. | application/problem+json: Problem |
| 503 | The platform could not answer, for example because the request took longer than 10 seconds or the instance was switching to another server. Ask again later. | application/problem+json: Problem |
Header on 429: Retry-After, integer. How many seconds to wait before sending the request again: a whole number, at least 1.
Header on 503: Retry-After, integer. Sent only while the instance is switching to another server: how many seconds to wait before sending the request again.
Example response, 200:
{
"deleted": false,
"external_id": "anna",
"fields": {
"assistant_name": {
"value": "Anna",
"visibility": "public"
},
"headline": {
"value": "Classic and sports massage, Baixa",
"visibility": "public"
},
"name": {
"value": "Anna, massage",
"visibility": "public"
},
"prices": {
"value": "Classic massage 60 minutes: 45 EUR. Sports massage 90 minutes: 60 EUR.",
"visibility": "ai_only"
},
"services": {
"value": "Classic massage, 60 minutes. Sports massage, 90 minutes.",
"visibility": "ai_only"
}
},
"private_fields": [
"phone"
],
"synced_at": "2026-10-12T09:20:00Z",
"unflagged_fields": [
"years_active"
],
"version": 42
}
PUT /v1/profiles/{external_id}
Upserts a profile.
Creates or replaces the platform's copy of one of the site's profiles. Send it each time a profile is created or changes on the site, with a version above the last one sent for that profile. For many profiles at once, such as an initial import, send them in batches of up to 100 with POST /v1/profiles/batch rather than one call each.
The platform applies the upsert only when its version is above the one it holds, a deleted profile's included. It answers 200 with the outcome: applied, unchanged (the same version and content again: a retry) or stale (a newer change arrived first). Neither unchanged nor stale is a failure. Private values are dropped on arrival, and only their field names are kept.
Safe to repeat: a retry carries the same version and content, so it cannot apply twice, and in any order the highest version wins. No idempotency key is needed. Keep each change queued until it gets a 200, and send it again after a 429 or a 5xx; any other error comes back the same until the request is fixed (contracts/profile-sync.md, "Ordering, retries and idempotency").
A rejection is a 422 whose type names the reason, and the partner console lists it for 30 days. An upsert answered 200 or rejected also keeps the site's profiles fresh for the AI assistant (contracts/profile-sync.md, "Keeping the copy fresh").
Send the site's secret key as Authorization: Bearer sk_....
Authentication: secret_key (see Authentication).
Parameters:
| Name | In | Type | Required | Description |
|---|---|---|---|---|
external_id |
path | string | Yes | The site's own id for the profile, such as anna: 1 to 128 ASCII letters, digits, -, _ and ., starting with a letter or a digit. Case matters. Never batch, which the batch route's path takes. Limits: 1 to 128 characters; matches ^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$. |
Request body (required): application/json: ProfileUpsert. The whole profile: its version, its owner and every field the platform should have. Fields left out are gone from the copy once the upsert applies.
Example request:
{
"fields": {
"assistant_name": {
"value": "Anna",
"visibility": "public"
},
"headline": {
"value": "Classic and sports massage, Baixa",
"visibility": "public"
},
"name": {
"value": "Anna, massage",
"visibility": "public"
},
"phone": {
"value": "+351 000 000 201",
"visibility": "private"
},
"prices": {
"value": "Classic massage 60 minutes: 45 EUR. Sports massage 90 minutes: 60 EUR.",
"visibility": "ai_only"
},
"services": {
"value": "Classic massage, 60 minutes. Sports massage, 90 minutes.",
"visibility": "ai_only"
},
"years_active": {
"value": "8"
}
},
"owner": "sg-owner-anna",
"version": 42
}
Responses:
| Status | When | Body |
|---|---|---|
| 200 | What the upsert did: applied, unchanged or stale, with the version the platform now holds. |
application/json: SyncResult |
| 400 | The request cannot be read, such as a body that is not valid JSON. Nothing applied. | application/problem+json: Problem |
| 401 | The secret key is missing or does not work, such as an unknown key, one ended in the partner console, or one a rotation replaced more than 24 hours ago. Nothing applied. | application/problem+json: Problem |
| 413 | The body is larger than 1 MiB (1,048,576 bytes). Nothing applied. | application/problem+json: Problem |
| 415 | The body was not sent as JSON. Send it with Content-Type: application/json. Nothing applied. |
application/problem+json: Problem |
| 422 | Rejected, with the reason as the type: /problems/owner-missing, /problems/invalid-field, /problems/too-large or /problems/version-reused, and field when the reason concerns one field whose name is valid. The partner console lists it for 30 days. Or the request cannot be read (about:blank, not listed): a version that is missing, not a whole number or out of range, an owner, fields or value of the wrong type, a field without value, or an external_id in the path that breaks its rule. Nothing applied either way: fix the request before sending it again. |
application/problem+json: RejectedProblem |
| 429 | Too many requests from this network address, or for this site: a site may send about 10 a second, with all its keys together. Nothing applied. Send the same request again after Retry-After seconds. |
application/problem+json: Problem |
| 500 | A fault on the platform's side. The upsert may or may not have applied: send the same request again later, which the version makes safe. | application/problem+json: Problem |
| 503 | The platform could not finish the request, for example because another request was writing the same profile, the request took longer than 10 seconds, or the instance was switching to another server. Usually nothing applied. Send the same request again later, which the version makes safe. | application/problem+json: Problem |
Header on 429: Retry-After, integer. How many seconds to wait before sending the request again: a whole number, at least 1.
Header on 503: Retry-After, integer. Sent only while the instance is switching to another server: how many seconds to wait before sending the request again.
Example response, 200:
{
"external_id": "anna",
"outcome": "applied",
"version": 42
}
Example response, 422:
{
"field": "assistant_name",
"status": 422,
"title": "Invalid field",
"type": "/problems/invalid-field"
}
DELETE /v1/profiles/{external_id}
Deletes a profile.
Erases the platform's copy of one of the site's profiles. Send it when a profile leaves the site, with a version above the profile's last upsert, since a delete is a change too. For many deletes at once, send them in batches with POST /v1/profiles/batch.
When the version is above the one held, the profile's fields and owner go at once. The platform keeps only the external id, the version and the time of the delete, so that an older upsert arriving late cannot bring the profile back. A delete of a profile the platform never had keeps that record too. To bring a deleted profile back, upsert it with a version above the delete's.
The answer is 200 with the same outcome an upsert gets. Safe to repeat: a retry carries the same version, so it cannot apply twice, and no idempotency key is needed. A delete answered 200 or rejected also keeps the site's profiles fresh for the AI assistant (contracts/profile-sync.md, "Keeping the copy fresh").
Send the site's secret key as Authorization: Bearer sk_....
Authentication: secret_key (see Authentication).
Parameters:
| Name | In | Type | Required | Description |
|---|---|---|---|---|
external_id |
path | string | Yes | The site's own id for the profile, such as anna: 1 to 128 ASCII letters, digits, -, _ and ., starting with a letter or a digit. Case matters. Never batch, which the batch route's path takes. Limits: 1 to 128 characters; matches ^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$. |
version |
query | integer | Yes | The delete's version, above the profile's last upsert, since a delete is a change too: 1 to 9,007,199,254,740,991. It goes in the query because some HTTP clients and proxies drop the body of a DELETE. A request without a version, or with text that is not a whole number, gets 400. A version outside that range gets 400 or 422, so send only versions within it. Limits: 1 to 9,007,199,254,740,991. |
Responses:
| Status | When | Body |
|---|---|---|
| 200 | What the delete did: applied, unchanged or stale, with the version the platform now holds. |
application/json: SyncResult |
| 400 | The request cannot be read, such as a query with no version or one that is not readable as a whole number. Nothing applied. |
application/problem+json: Problem |
| 401 | The secret key is missing or does not work, such as an unknown key, one ended in the partner console, or one a rotation replaced more than 24 hours ago. Nothing applied. | application/problem+json: Problem |
| 422 | Rejected as /problems/version-reused: the platform already holds an upsert with this version. The partner console lists it for 30 days. Or the request cannot be read (about:blank, not listed): a version out of range, such as 0, or an external_id in the path that breaks its rule. Nothing applied either way: fix the request before sending it again. |
application/problem+json: RejectedProblem |
| 429 | Too many requests from this network address, or for this site: a site may send about 10 a second, with all its keys together. Nothing applied. Send the same request again after Retry-After seconds. |
application/problem+json: Problem |
| 500 | A fault on the platform's side. The delete may or may not have applied: send the same request again later, which the version makes safe. | application/problem+json: Problem |
| 503 | The platform could not finish the request, for example because another request was writing the same profile, the request took longer than 10 seconds, or the instance was switching to another server. Usually nothing applied. Send the same request again later, which the version makes safe. | application/problem+json: Problem |
Header on 429: Retry-After, integer. How many seconds to wait before sending the request again: a whole number, at least 1.
Header on 503: Retry-After, integer. Sent only while the instance is switching to another server: how many seconds to wait before sending the request again.
Example response, 200:
{
"external_id": "anna",
"outcome": "applied",
"version": 43
}
Example response, 422:
{
"status": 422,
"title": "Version reused",
"type": "/problems/version-reused"
}
Schemas
BatchItem
One item of a batch, by its action: an upsert, which is the body of a PUT plus the profile's external_id, or a delete, with external_id and version.
One of these forms:
action is upsert. An upsert: the body of PUT /v1/profiles/{external_id}, plus the profile's external_id.
Every field of ProfileUpsert, and these:
| Field | Type | Required | Description |
|---|---|---|---|
external_id |
string | Yes | The site's own id for the profile, with the same rule as in the path of a PUT: 1 to 128 ASCII letters, digits, -, _ and ., starting with a letter or a digit, never batch. Limits: 1 to 128 characters; matches ^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$. |
action |
upsert |
Yes |
action is delete. A delete, as DELETE /v1/profiles/{external_id} makes it.
| Field | Type | Required | Description |
|---|---|---|---|
action |
delete |
Yes | |
external_id |
string | Yes | The site's own id for the profile, with the same rule as in the path of a DELETE: 1 to 128 ASCII letters, digits, -, _ and ., starting with a letter or a digit, never batch. Limits: 1 to 128 characters; matches ^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$. |
version |
integer | Yes | The delete's version, above the profile's last upsert: 1 to 9,007,199,254,740,991. Limits: 1 to 9,007,199,254,740,991. |
BatchItemResult
What one item of a batch did, by its outcome: applied, unchanged or stale, as PUT and DELETE answer them, or rejected, with the same reason a PUT or a DELETE would get.
One of these forms:
outcome is applied. Its version was above the one held, or none was: it replaced the copy.
| Field | Type | Required | Description |
|---|---|---|---|
external_id |
string | Yes | The profile, by the site's id for it. |
outcome |
applied |
Yes | |
version |
integer | Yes | The version the platform holds now: the item's. Limits: 1 to 9,007,199,254,740,991. |
outcome is unchanged. The same version and content as the copy held: a retry, which changed nothing.
| Field | Type | Required | Description |
|---|---|---|---|
external_id |
string | Yes | The profile, by the site's id for it. |
outcome |
unchanged |
Yes | |
version |
integer | Yes | The version the platform holds now: the item's. Limits: 1 to 9,007,199,254,740,991. |
outcome is stale. A lower version than the one held: a newer change arrived first, and this one changed nothing.
| Field | Type | Required | Description |
|---|---|---|---|
external_id |
string | Yes | The profile, by the site's id for it. |
outcome |
stale |
Yes | |
version |
integer | Yes | The version the platform holds now, higher than the item's. Limits: 1 to 9,007,199,254,740,991. |
outcome is rejected. Rejected, and listed in the partner console for 30 days. The item changed nothing; fix it before sending it again.
| Field | Type | Required | Description |
|---|---|---|---|
external_id |
string | Yes | The profile, by the site's id for it. |
field |
string | No | The first field, in the order of the field names, that broke a rule, when the reason concerns one field whose name is valid. Absent otherwise. Limits: matches ^[a-z][a-z0-9_]{0,63}$. |
outcome |
rejected |
Yes | |
reason |
RejectionReason | Yes | Why the item was rejected. |
FieldVisibility
The visibility flags a site may send with a field. public: kept, read by the AI assistant, and shown on the widget's profile card for name, headline and assistant_name. ai_only: kept, and read only by the AI assistant, which may repeat it to visitors. private: only the field's name is kept; the value is dropped on arrival and nothing reads it. Any other value, or none, is private.
A string, one of:
publicai_onlyprivate
KeptField
A field whose value the platform keeps: a public or an AI-only one.
| Field | Type | Required | Description |
|---|---|---|---|
value |
string | Yes | The value, as the site sent it. |
visibility |
KeptVisibility | Yes | Who may see it: public or ai_only. |
KeptVisibility
The visibility of a field whose value the platform keeps. public: the AI assistant reads it, and the widget's profile card shows it for name, headline and assistant_name. ai_only: only the AI assistant reads it. A private value is never kept.
A string, one of:
publicai_only
Problem
An error, as an RFC 9457 problem document.
| Field | Type | Required | Description |
|---|---|---|---|
status |
integer | Yes | The HTTP status code. Limits: at least 0. Example: 503. |
title |
string | Yes | The standard phrase for the status code. Example: Service Unavailable. |
type |
string | Yes | Always about:blank: the status code says what went wrong. Example: about:blank. |
ProfileBatch
Up to 100 upserts and deletes of the site's profiles, each decided on its own: a rejected item stops no other. The batch is saved as one: its changes take effect together, or none does.
| Field | Type | Required | Description |
|---|---|---|---|
items |
array of BatchItem | Yes | The upserts and deletes, 1 to 100, each with its action. Items that name the same profile apply in the order sent. When one cannot be read (an unknown action, an external_id that breaks its rule or is batch, a version missing or out of range), the whole batch gets 422 and none of it applies. Limits: 1 to 100 items. |
total |
integer | No | Optional: how many profiles the site lists in all, from 0 to 9,007,199,254,740,991. The partner console shows "imported of total" from the last batch that sent it, with its time. Send it during an initial import and whenever it changes. Limits: 0 to 9,007,199,254,740,991. |
ProfileBatchResults
What each item of a batch did.
| Field | Type | Required | Description |
|---|---|---|---|
results |
array of BatchItemResult | Yes | One result for each item, in the items' order. |
ProfileCopy
The platform's copy of one profile, for the site to check what arrived. It never holds a private value, which the platform drops on arrival, nor the owner, of whom the platform keeps only a fingerprint.
| Field | Type | Required | Description |
|---|---|---|---|
deleted |
boolean | Yes | Whether that change was a delete. A deleted profile has no fields: fields, private_fields and unflagged_fields are empty. |
external_id |
string | Yes | The profile, by the site's id for it. |
fields |
object, from a name to KeptField | Yes | The public and AI-only fields, from their name to the field, with their values as sent: what the AI assistant reads. |
private_fields |
array of string | Yes | The names of the fields sent as private. Their values were dropped on arrival. |
synced_at |
string (date-time) | Yes | When that upsert or delete arrived: an RFC 3339 date-time in UTC, which may carry a fraction of a second. |
unflagged_fields |
array of string | Yes | The names of the fields sent with no visibility, with null, or with one the platform does not recognise. They are private too, and their values were dropped on arrival. |
version |
integer | Yes | The version of the last upsert or delete the platform applied. Limits: 1 to 9,007,199,254,740,991. |
ProfileUpsert
A profile as the site sends it: its version, its owner and its fields. It replaces the platform's copy when its version is above the one held, a deleted profile's included (contracts/profile-sync.md, "Upsert").
| Field | Type | Required | Description |
|---|---|---|---|
fields |
object, from a name to SentField | Yes | The profile's fields, from a field name to the field: 1 to 100 of them, name among them. A field name matches ^[a-z][a-z0-9_]{0,63}$. The public and AI-only values hold at most 16,000 characters in all; private values do not count, since the platform drops them. Breaking a rule rejects the upsert as invalid-field or too-large (RejectionReason). A missing or null fields has no name, so it is rejected as invalid-field. SentField says what each visibility keeps and feeds. |
owner |
string | Yes | The profile owner, who answers for the profile in the inbox: the same user ID the site puts in sub when it signs that person in (contracts/user-token.md). 1 to 128 characters. Never an email address, a phone number or a name. The platform keeps only a fingerprint of it, made with the site, and never returns it. An applied upsert with another owner gives the profile and its past conversations to the new owner, and the previous owner loses access to them at once. Missing, null, empty or too long: the upsert is rejected as owner-missing. Limits: 1 to 128 characters. |
version |
integer | Yes | The profile's version, which the site chooses: a whole number from 1 to 9,007,199,254,740,991, the largest whole number JavaScript reads exactly. It grows with every change to the profile, deletes included, such as a counter or the time of the change in milliseconds. The platform applies a change only when its version is above the one it holds. The same version again with the same content is a retry (unchanged); with other content it is rejected as version-reused. A version that is missing, not a whole number or out of range gets 422 (about:blank) and is not listed in the console. Limits: 1 to 9,007,199,254,740,991. |
ProfileVersion
A profile the platform holds and the version of its last change, so the site can send again what differs.
| Field | Type | Required | Description |
|---|---|---|---|
deleted |
boolean | Yes | Whether that change was a delete. |
external_id |
string | Yes | The profile, by the site's id for it. |
version |
integer | Yes | The version of the last upsert or delete the platform applied. Limits: 1 to 9,007,199,254,740,991. |
ProfileVersionPage
One page of the profiles the platform holds for the site, deleted ones included, in byte order of external_id.
| Field | Type | Required | Description |
|---|---|---|---|
next |
string | No | The after for the next page: the last external id on this one. Absent on the last page. |
profiles |
array of ProfileVersion | Yes | The profiles on this page, at most limit. |
RejectedProblem
A 422 on a route that upserts or deletes a profile: a rejection, whose type names the reason and which the partner console lists for 30 days, or a request the platform could not read (about:blank, a plain problem), which the console does not list. Either way nothing applied, and the same request gets the same answer until it is fixed.
| Field | Type | Required | Description |
|---|---|---|---|
field |
string | No | The first field that broke a rule, when the reason concerns one field whose name is valid. Absent otherwise. Limits: matches ^[a-z][a-z0-9_]{0,63}$. |
status |
integer | Yes | Always 422. Limits: at least 0. Example: 422. |
title |
string | Yes | What the type means, in a few words. Example: Version reused. |
type |
one of about:blank, /problems/owner-missing, /problems/invalid-field, /problems/version-reused, /problems/too-large |
Yes | about:blank when the body could not be read, such as a version out of range, and nothing is listed in the console; otherwise the reason for the rejection, which the console lists for 30 days: /problems/owner-missing when the upsert has no usable owner; /problems/invalid-field when a field name breaks its pattern, a value is empty or holds U+0000, there is no name field, or assistant_name is longer than 60 characters, has a line break or holds an invisible formatting character; /problems/version-reused when the version is the one held, with other content; /problems/too-large when there are more than 100 fields, a value is longer than 4,000 characters or the public and AI-only values hold more than 16,000 characters. Compare it as written. |
RejectionReason
Why the platform rejected an upsert or a delete. The partner console lists each rejection for 30 days, with the field when there is one, never the content (contracts/profile-sync.md, "Rejections"). owner-missing: the upsert has no owner, or it is empty or longer than 128 characters. invalid-field: a field name breaks its pattern, a value is empty or holds U+0000, there is no name field, or assistant_name is longer than 60 characters, has a line break or holds an invisible formatting character. too-large: more than 100 fields, a value longer than 4,000 characters, or more than 16,000 characters of public and AI-only values in all. version-reused: the version is the one held, with other content; give every change a new version.
A string, one of:
owner-missinginvalid-fieldversion-reusedtoo-large
SentField
One field of a profile, as the site sends it: its value, and who may see it.
| Field | Type | Required | Description |
|---|---|---|---|
value |
string | Yes | The field's text: 1 to 4,000 characters, without U+0000. Lengths count characters (Unicode code points), not bytes. The rule holds for private values too, though the platform drops them on arrival and never stores, shows or gives them to the AI assistant. assistant_name has a stricter rule, whatever its visibility: 1 to 60 characters on one line, without invisible formatting characters. Sent as public, it names the AI assistant in the label on its replies, such as "Anna's AI assistant". Without it, the label uses a public name that keeps the same rule; with neither, the profile gets no AI replies (contracts/profile-sync.md, "The name in the AI label"). Limits: 1 to 4,000 characters. |
visibility |
FieldVisibility or null | No | Who may see the value, and so what it feeds. public: the platform keeps it, the AI assistant reads it, and visitors may see it. The widget's profile card shows name, headline and assistant_name when they are public, and nothing else from the profile. ai_only: the platform keeps it, and only the AI assistant reads it, to answer visitors. The AI assistant may repeat it to them, so put nothing there that a visitor must not read. private: the platform keeps the field's name only and drops the value on arrival, so nothing reads it. Left out, null, or a value the platform does not recognise, such as "Public" or "hidden": private, and the partner console counts the field as sent without a flag. |
SyncOutcome
What an upsert or a delete the platform accepted did. applied: its version was above the one held, or none was, and it replaced the copy. unchanged: the same version and the same content as the copy held, so it was a retry and changed nothing. stale: a lower version than the one held, so a newer change arrived first and this one changed nothing. Neither unchanged nor stale is a failure, and neither needs anything sent again.
A string, one of:
appliedunchangedstale
SyncResult
What an upsert or a delete did (contracts/profile-sync.md, "Versions"). A rejection is a 422 problem instead, whose type names the reason.
| Field | Type | Required | Description |
|---|---|---|---|
external_id |
string | Yes | The profile, by the site's id for it, as in the path. |
outcome |
SyncOutcome | Yes | What happened: applied, unchanged or stale. |
version |
integer | Yes | The version the platform holds now: the one sent when applied or unchanged, a higher one when stale. Limits: 1 to 9,007,199,254,740,991. |