Skip to main content
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.
  • POST never reactivates. A deactivated prompt with the same text does not block the create, so POST adds a second prompt beside it and the history starts again. To bring back one prompt without a sync, send PATCH with "active": true.
When an active prompt with the same text already exists, 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.
Some problems refuse the whole request, and then nothing is applied:

How rows are matched

A row matches an existing prompt by its text, 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 a text field is 400 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.
Once a row has matched, the API decides whether anything differs:
  • countries are compared as a set. Order and case do not matter.
  • platforms and tags are compared only when you send them.
  • Only the fields that differ are written.
Prompts added outside the API can differ only by whitespace at the ends, so a brand can hold several active prompts that match one text. The sync takes the oldest as the match. It treats the others as prompts you did not name, so deactivate_missing deactivates them.

The actions

Every entry of results 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

With deactivate_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 200 therefore describes the brand’s real active set.
With deactivate_missing: false, the sync only creates, updates and reactivates. It switches nothing off.
The default is true. If you send only part of a brand’s prompts, every other active prompt of that brand is deactivated. When other people add prompts to the same brand in the dashboard and you want to keep them, send "deactivate_missing": false.

Not atomic: read the response

A 200 does not mean that every row was applied. Rows are applied one at a time, and a row that is refused comes back as failed inside the 200, while the rest of the request still runs. Always check summary.failed.
A failed 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_missing on. The exceptions are prompt_changed and not_found, where the prompt is no longer the one your text matched. A renamed prompt can therefore appear twice: once as failed with prompt_changed, and once as deactivated, because its new text is not in your list.
  • Any other failure ends the whole request with 500 internal_error, or with 503 store_unavailable when 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 as unchanged.

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.
This request names five prompts for a brand that also has one active prompt the list leaves out:
The numbers in this example are illustrative. Do not parse 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.
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.
A dry run, with no key:
cURL
The apply call. The Python and Node.js samples retry the failures that are safe to retry, always with the same key, and then report every failed row.
Rows are applied one at a time, so give a long list a generous client timeout. If your client gives up first, the sync carries on at the server. Retry with the same Idempotency-Key: you get 409 idempotency_key_in_flight while it is still running, and then the stored response. See Idempotent requests.

Caps

The sync never fails as a whole because of a plan cap. A cap refuses single rows, and they come back as failed 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 reactivated row can fail.
Read where you stand before you send a list:
  • usage in every sync response, including a dry run;
  • usage on GET /orgs/{orgId}, which also reports the brand cap and the per-brand competitor cap.
Plans & limits lists the caps of each plan. Setting up your prompts and Countries & markets explain how to choose what to track.

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.