curl --request PUT \
--url https://api.surfais.com/v1/orgs/{orgId}/brands/{brandId}/prompts \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"prompts": [
{
"text": "<string>",
"countries": [
"<string>"
],
"platforms": [],
"tags": [
"<string>"
]
}
],
"deactivate_missing": true,
"dry_run": false
}
'import requests
url = "https://api.surfais.com/v1/orgs/{orgId}/brands/{brandId}/prompts"
payload = {
"prompts": [
{
"text": "<string>",
"countries": ["<string>"],
"platforms": [],
"tags": ["<string>"]
}
],
"deactivate_missing": True,
"dry_run": False
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.put(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'PUT',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
prompts: [{text: '<string>', countries: ['<string>'], platforms: [], tags: ['<string>']}],
deactivate_missing: true,
dry_run: false
})
};
fetch('https://api.surfais.com/v1/orgs/{orgId}/brands/{brandId}/prompts', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));{
"data": {
"summary": {
"created": 4503599627370495,
"updated": 4503599627370495,
"reactivated": 4503599627370495,
"unchanged": 4503599627370495,
"reasserted": 4503599627370495,
"reinstated": 4503599627370495,
"deactivated": 4503599627370495,
"failed": 4503599627370495
},
"results": [
{
"text": "<string>",
"action": "created",
"prompt_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"error": {
"code": "<string>",
"message": "<string>"
}
}
],
"usage": {
"prompts": {
"used": 0,
"cap": 0
},
"countries": {
"used": [
"<string>"
],
"cap": 0
}
}
}
}{
"error": {
"code": "<string>",
"message": "<string>",
"details": "<unknown>"
},
"request_id": "<string>"
}{
"error": {
"code": "<string>",
"message": "<string>",
"details": "<unknown>"
},
"request_id": "<string>"
}{
"error": {
"code": "<string>",
"message": "<string>",
"details": "<unknown>"
},
"request_id": "<string>"
}{
"error": {
"code": "<string>",
"message": "<string>",
"details": "<unknown>"
},
"request_id": "<string>"
}{
"error": {
"code": "<string>",
"message": "<string>",
"details": {
"brand_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a"
}
},
"request_id": "<string>"
}{
"error": {
"code": "<string>",
"message": "<string>",
"details": "<unknown>"
},
"request_id": "<string>"
}{
"error": {
"code": "<string>",
"message": "<string>",
"details": "<unknown>"
},
"request_id": "<string>"
}{
"error": {
"code": "<string>",
"message": "<string>",
"details": "<unknown>"
},
"request_id": "<string>"
}{
"error": {
"code": "<string>",
"message": "<string>",
"details": "<unknown>"
},
"request_id": "<string>"
}Sync prompts
Declaratively sync an own brand’s active prompt set.
Makes the brand’s ACTIVE prompts equal prompts (at most 500 rows). Matching is on the text, case- and surrounding-whitespace-insensitive, within the brand (coarser than the lower(text) uniqueness index, so several ACTIVE rows can share a key: the oldest is the match and the rest count as rows the request did not name). Per desired row: an active match with identical countries (and platforms / tags where sent) is unchanged; an active match that differs is updated; a text that exists only INACTIVE reactivates the newest inactive row (reactivated, fields updated) — never a duplicate insert; anything else is created (source: "api"). With deactivate_missing (default true) every active prompt not in the request is deactivated. Each desired row is asserted ACTIVE in the same statement that writes it (a row planned as unchanged is asserted conditionally, so a clean sync still writes nothing), which is what makes a downgrade landing mid-run refuse the row instead of editing a prompt it has already switched off: expect failed / brand_archived for those rows, and reactivated where a row planned as unchanged had been switched off on a still-live brand. NOT ATOMIC: rows are applied one by one, deactivations FIRST (so the slots they free are available to the creates and reactivations that follow), then the desired rows in request order; a row refused by a cap (422 codes), by the archived-brand rule (brand_archived) or by the active-text uniqueness rule is reported as failed with its code and the rest of the request still runs. The LAST statement of a deactivate_missing run re-reads the brand’s active set under a brand-grain lock every writer of a live prompt takes: it switches off anything outside the requested set, and RE-ASSERTS the DESIRED STATE of the rows this call applied — their countries / platforms / tags (each restored row reported reasserted) and the two claims underneath those, that the row still exists and is still ACTIVE. The sync’s per-row writes and that reconciliation are separate transactions, so a concurrent PATCH, DELETE or deactivation landing between them would otherwise leave the 200 describing a state that had been overwritten. A prompt switched off in that window is put back on with its fields, reported reinstated; a prompt DELETED in it is reported failed / not_found and is not re-created (its text was resolved to an id that no longer names anything — re-run the sync and it is created properly). Re-activation is cap-checked like a create, so one the database refuses is one more failed row and the rest of the reconciliation still lands. A refused row is never re-asserted (nothing was applied to it), and a row whose text another writer changed after it was written is reported failed / prompt_changed and deactivated rather than rewritten. So when this endpoint answers 200, every prompt it says it applied is present, active and carrying the requested fields, or named in results as failed. The response is 200 with a per-row results list (request order, then the deactivated, reasserted and reinstated rows), a summary and the organisation’s usage afterwards. dry_run returns the same shape with the planned actions and writes nothing. Duplicate texts within the request are 400 duplicate_prompt_text. Own brands only (400 validation_error with details[].code = "own_brand_only"); an archived brand is 409 brand_archived, an empty sync included.
curl --request PUT \
--url https://api.surfais.com/v1/orgs/{orgId}/brands/{brandId}/prompts \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"prompts": [
{
"text": "<string>",
"countries": [
"<string>"
],
"platforms": [],
"tags": [
"<string>"
]
}
],
"deactivate_missing": true,
"dry_run": false
}
'import requests
url = "https://api.surfais.com/v1/orgs/{orgId}/brands/{brandId}/prompts"
payload = {
"prompts": [
{
"text": "<string>",
"countries": ["<string>"],
"platforms": [],
"tags": ["<string>"]
}
],
"deactivate_missing": True,
"dry_run": False
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.put(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'PUT',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
prompts: [{text: '<string>', countries: ['<string>'], platforms: [], tags: ['<string>']}],
deactivate_missing: true,
dry_run: false
})
};
fetch('https://api.surfais.com/v1/orgs/{orgId}/brands/{brandId}/prompts', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));{
"data": {
"summary": {
"created": 4503599627370495,
"updated": 4503599627370495,
"reactivated": 4503599627370495,
"unchanged": 4503599627370495,
"reasserted": 4503599627370495,
"reinstated": 4503599627370495,
"deactivated": 4503599627370495,
"failed": 4503599627370495
},
"results": [
{
"text": "<string>",
"action": "created",
"prompt_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"error": {
"code": "<string>",
"message": "<string>"
}
}
],
"usage": {
"prompts": {
"used": 0,
"cap": 0
},
"countries": {
"used": [
"<string>"
],
"cap": 0
}
}
}
}{
"error": {
"code": "<string>",
"message": "<string>",
"details": "<unknown>"
},
"request_id": "<string>"
}{
"error": {
"code": "<string>",
"message": "<string>",
"details": "<unknown>"
},
"request_id": "<string>"
}{
"error": {
"code": "<string>",
"message": "<string>",
"details": "<unknown>"
},
"request_id": "<string>"
}{
"error": {
"code": "<string>",
"message": "<string>",
"details": "<unknown>"
},
"request_id": "<string>"
}{
"error": {
"code": "<string>",
"message": "<string>",
"details": {
"brand_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a"
}
},
"request_id": "<string>"
}{
"error": {
"code": "<string>",
"message": "<string>",
"details": "<unknown>"
},
"request_id": "<string>"
}{
"error": {
"code": "<string>",
"message": "<string>",
"details": "<unknown>"
},
"request_id": "<string>"
}{
"error": {
"code": "<string>",
"message": "<string>",
"details": "<unknown>"
},
"request_id": "<string>"
}{
"error": {
"code": "<string>",
"message": "<string>",
"details": "<unknown>"
},
"request_id": "<string>"
}Authorizations
A Surfais API key — an organisation key or a partner key. Issued by Surfais; shown once. Rotate by creating a new key, then revoking the old one.
Headers
Optional. 1–255 characters, unique per intended write. Same key + same request → the stored response is replayed with Idempotent-Replayed: true; same key + different request → 409 idempotency_key_reuse; still running → 409 idempotency_key_in_flight. Outside that range → 400 validation_error (invalid_header).
1 - 255Path Parameters
Organisation id. Org keys: the key's own org. Partner keys: any org with an active link. Anything else is 404.
^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$Brand id within the org (own brand or competitor row); 404 when not in the org. Writes that add tracking need an own (400 validation_error with details[].code = "own_brand_only"), non-archived (409 brand_archived) brand.
^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$Body
The desired ACTIVE prompt set for the brand, at most 500 rows. Two rows with the same text (case- and whitespace-insensitive) are 400 duplicate_prompt_text. An empty list with deactivate_missing: true deactivates every active prompt of the brand.
500Show child attributes
Show child attributes
Deactivate active prompts of the brand that are not in prompts. Default true.
Return the plan (results[].action per row) without writing anything.
Response
Multi-status result: what was created / updated / reactivated / unchanged / reasserted / reinstated / deactivated / failed.
Show child attributes
Show child attributes