PUT /orgs/{orgId}/brands/{brandId}/prompts is declarative. You send the set of prompts you want active for one own brand. The API compares that set with what the brand has and makes the changes: it creates what is new, updates what differs, switches back on what was deactivated earlier and, by default, deactivates what you left out.
The call needs a key with the
write scope. A partner key also needs a read_write link to the organisation. See Authentication.When to use the sync
The sync treats a returning prompt differently from
POST, and the difference matters for history:
- The sync reactivates. When the text you send exists only as a deactivated prompt, the sync switches that prompt back on, so its id and its history carry on.
POSTnever reactivates. A deactivated prompt with the same text does not block the create, soPOSTadds a second prompt beside it and the history starts again. To bring back one prompt without a sync, sendPATCHwith"active": true.
POST answers 409 conflict with details.existing_id. The sync reports that prompt as unchanged or updated instead.
Request body
object[]
required
The prompts you want active for the brand. At most 500 rows per call; a longer list is
400 validation_error on prompts.The list may be empty. With deactivate_missing on, an empty list deactivates every active prompt of the brand.string
required
The prompt, written the way a person would type it into an AI assistant. 1 to 500 characters. Leading and trailing whitespace is trimmed.
string[]
required
The markets the prompt runs in, as ISO 3166-1 alpha-2 codes such as
IE. At least one. Case does not matter; codes are upper-cased and duplicates are dropped. A code Surfais does not scan is 400 validation_error with details[].code set to unsupported_country.string[]
The AI platforms the prompt runs on: any of
perplexity, chatgpt, gemini, claude and ai_overviews. At least one when you send the field.Leave it out of a new prompt to run on every platform. Leave it out of an existing prompt to keep its platforms as they are.string[]
Free-text labels. At most 50 tags on a prompt. Each tag is 1 to 100 characters, and at most 256 bytes of UTF-8, after trimming, and holds no control characters. An empty tag is refused. Duplicates are dropped. Case is kept, so
Launch and launch are two tags.Leave it out of a new prompt for no tags. Leave it out of an existing prompt to keep its tags as they are.boolean
default:"true"
Deactivate every active prompt of the brand that is not in
prompts. See Deactivating what you left out.boolean
default:"false"
Return the planned action for every row and write nothing.
How rows are matched
A row matches an existing prompt by itstext, within the brand, ignoring case and ignoring whitespace at either end. Best boutique hotels in Dublin and best boutique hotels in dublin are the same prompt. Everything between the first and last character counts, including spacing and punctuation.
Text is identity. No operation edits a prompt’s wording:
- To reword a prompt, send the new text and leave the old text out. The new text becomes a new prompt. The old prompt is deactivated and keeps the results it has collected.
PATCH /orgs/{orgId}/prompts/{promptId}with atextfield is400 text_immutable.- The sync never rewrites stored text. If you send a different capitalisation of an existing prompt, the row matches and the stored spelling stays.
countriesare compared as a set. Order and case do not matter.platformsandtagsare compared only when you send them.- Only the fields that differ are written.
The actions
Every entry ofresults carries one action.
“Another writer” is anyone else changing the brand’s prompts at that moment: a colleague in the dashboard, or another API call.
reasserted and reinstated are rare, and they appear only when deactivate_missing is true: they come from a final check that such a sync makes before it answers. Each one is an extra entry in results. The prompt keeps its own entry for the action it was first applied as, and summary counts the two separately. When nothing failed, created + updated + reactivated + unchanged equals the number of prompts you sent.
Deactivating what you left out
Withdeactivate_missing: true, which is the default, the brand’s active set ends up equal to your list. Every active prompt you did not name is deactivated.
- Deactivations run first. The slots they free are available to the creates and reactivations later in the same call. An organisation at its prompt cap or country cap can therefore rotate its prompts in one request.
- Deactivating never deletes. The prompt and its history stay. It stops being scanned, and it stops taking up a slot under the plan’s caps. The API has no call that deletes a prompt.
- The last step checks the result. Just before it answers, the sync reads the brand’s active prompts again and switches off any that are not in your list — including one that another writer added while the sync was running. A
200therefore describes the brand’s real active set.
deactivate_missing: false, the sync only creates, updates and reactivates. It switches nothing off.
Not atomic: read the response
Afailed row carries its own error. These are the codes to expect:
Treat any other code the same way: nothing was applied to that row.
Two more rules make the outcome predictable:
- A refused row is left exactly as it was. If the prompt already existed and was active, it stays active with the fields it had. A failed update never deactivates the prompt, even with
deactivate_missingon. The exceptions areprompt_changedandnot_found, where the prompt is no longer the one your text matched. A renamed prompt can therefore appear twice: once asfailedwithprompt_changed, and once asdeactivated, because its new text is not in your list. - Any other failure ends the whole request with
500 internal_error, or with503 store_unavailablewhen the data store did not answer in time. Rows applied before the failure stay applied, and the response never claims more than was done. Run the sync again. It is idempotent by design: the API builds the plan again from the brand’s current state, and rows that were already applied come back asunchanged.
The response
object
Eight counters, one per action:
created, updated, reactivated, unchanged, reasserted, reinstated, deactivated and failed.object[]
One entry per prompt you sent, in request order, then one entry per prompt that was deactivated. Entries added by the final check come last:
reasserted, reinstated, and any late failed or deactivated.Each entry has text and action. prompt_id is present whenever the entry maps to an existing prompt, and on created entries after a real run. error is present on failed entries. For the prompts you sent, text is the text you sent; on the other entries it is the stored text.object
The organisation’s usage after the run.
prompts.used is the number of active prompts in the whole organisation (on the Agency plan, the prompt-country slots they use: one for every country each prompt is tracked in), and countries.used lists the distinct markets those prompts hold. Each has a cap, which is null when caps are not enforced for the organisation.message; the organisation’s current caps are whatever usage reports here and on GET /orgs/{orgId}.
A dry_run returns the same shape with the planned actions. Its created entries carry no prompt_id, because nothing was created, and its usage is the usage before the run.
Recommended flow
1
Dry run
Send your list with
"dry_run": true. The API answers with the action it plans for every row and writes nothing.2
Inspect the summary
Check that the counts are the ones you expect, above all
deactivated. A list that was cut short upstream shows up here as a large number of deactivations, before any of them has happened.A dry run does not test the caps. A row planned as created can still come back failed when you apply it. Compare usage with the rows you are adding to see how much room there is.3
Apply with an Idempotency-Key
Send the same list without
dry_run, and with an Idempotency-Key header. Use a new key, not the dry run’s: the two bodies differ, and one key on two different requests is 409 idempotency_key_reuse.4
Check the failed rows
Read
summary.failed. For every failed entry, read error.code and decide what to do from the table above.5
Run it again if you need to
Once the cause is fixed, send the list again with a new key. The same key with the same body replays the stored
200 — the response carries Idempotent-Replayed: true — and applies nothing. Rows that were applied the first time come back as unchanged.cURL
failed row.
Caps
The sync never fails as a whole because of a plan cap. A cap refuses single rows, and they come back asfailed with prompt_cap_exceeded or country_cap_exceeded. The single-prompt endpoints answer the same refusals as a 422 with the same codes. There, details can carry tier, cap and attempted, and each one is optional.
What the two caps count:
- The prompt cap counts the active prompts of the whole organisation, across all its own brands — not per brand.
- The country cap counts the distinct countries held by those active prompts, across the whole organisation.
- A deactivated prompt frees its slot. Reactivating one is checked against the caps exactly like a create, which is why a
reactivatedrow can fail.
usagein every sync response, including a dry run;usageonGET /orgs/{orgId}, which also reports the brand cap and the per-brand competitor cap.
Next steps
Run a scan
Request a scan once the brand has active prompts.
Export results
Read each prompt’s results: which brands were mentioned, and which sources were cited.