---
title: "Bulk-edit prompts"
method: POST
path: "/api/mcp/projects/{projectId}/prompts/sync"
tags: ["Prompts"]
---

# Bulk-edit prompts

`POST /api/mcp/projects/{projectId}/prompts/sync`

## Request

- Base URL: `https://app.searchable.com`
- URL: `https://app.searchable.com/api/mcp/projects/{projectId}/prompts/sync`
- Auth: HTTP bearer

Edit existing prompts in bulk, in place: the round trip for a spreadsheet of prompts. Read them with `GET /projects/{projectId}/prompts` (each row carries its `id` and `currentRevision`; `?format=csv` downloads them), change what you need, and send up to 500 rows back in `prompts`. Each row names a prompt by `promptId` and gives the end state for any of `text`, `isBranded`, `intentCategory`, `topics` (names; the list replaces the prompt's topics, `[]` removes them, names the project lacks are created) and `locations` (the full set, at most 5; a location left out stops being tracked and its history is kept). Omit a field to leave it alone; a row that changes nothing writes nothing. Send each prompt's `expectedRevision`: a prompt edited since you read it comes back as `conflict` instead of being overwritten. Every row of one prompt must be in the same request. A reworded prompt keeps its id and past answers, and its old wording stays as a revision. A batch whose added locations do not fit the prompt allowance is refused with 403 `quota_exceeded` and nothing written; past that, each prompt stands alone and reports its own `outcome`. Result entries carry each `promptId` trimmed and lower-cased, so match ids case-insensitively. New prompts go through `POST /projects/{projectId}/prompts`. `dryRun: true` returns the same plan without writing. A dry run plans the edit but does not check the prompt allowance; adding locations to tracked prompts can still be refused with `quota_exceeded` when you apply. Requires a connection minted with the `prompt:draft` capability, and your own project role must allow the write (otherwise 403 `forbidden`). A pitch project that is not currently active refuses with 409 `INACTIVE_PITCH_PROMPT_MUTATION`. Supports `Idempotency-Key` replay; a reused key with a different body returns 409 `idempotency_conflict`.

## Path parameters

- `projectId` string, uuid, required

## Headers

- `Idempotency-Key` string

## Request body

- object
  - `prompts` object[], required
    - `promptId` string, required: The prompt's id.
    - `expectedRevision` integer: The prompt's `currentRevision` when you read it.
    - `text` string: The complete new wording, 5 to 500 characters after trimming.
    - `isBranded` boolean
    - `intentCategory` 'informational' | 'commercial' | 'transactional' | 'navigational' | 'post_purchase'
    - `topics` string[]: Topic names, at most 10 of up to 100 characters each; replaces the prompt's topics. An empty list removes them all. A list of 11 to 50 names is accepted, but that prompt comes back `invalid`.
    - `locations` object[]: The full location set, at most 5 after merging a prompt's rows. A location the prompt already has is kept as it is, and a US state's code matches the name it holds. A `state` with no `city` is tracked as that state, written as its name; a US state may also be its two-letter code, but a code elsewhere, such as `ON` or `NSW`, is not recognised. A city that is not recognised is tracked country-wide. A state that matches no known state, and a US city sent without a `state` that exists in more than one state, are not tracked; each adds a note to the prompt's entry. A prompt left with no location it can track keeps the ones it has. A city or state that could not be checked, because the lookup failed or did not answer, leaves all of the prompt's locations as they are: nothing is added or closed, so the sync cannot take the prompt above 5, and its entry carries only the notes for the places that could not be checked. Such a city is not tracked country-wide. The prompt's other fields still change. Send the rows again to check them.
      - `country` string, required: ISO 3166-1 alpha-2 code or country name.
      - `city` string
      - `state` string
  - `dryRun` boolean: Return the plan without writing.

## Response `200`

Each named prompt's outcome.

- object
  - `dryRun` boolean
  - `counts` object
    - `changed` integer
    - `unchanged` integer
    - `conflict` integer
    - `not_found` integer
    - `invalid` integer
    - `failed` integer
  - `prompts` object[]
    - `promptId` string
    - `outcome` 'changed' | 'unchanged' | 'conflict' | 'not_found' | 'invalid' | 'failed'
    - `text` string: The prompt's wording before this change.
    - `changes` object: Per changed field (`text`, `isBranded`, `intentCategory`, `topics`, `locations`): `{ before, after }`.
    - `currentRevision` integer: For `conflict`, the prompt's revision now; for an applied `changed`, the revision it reached.
    - `currentText` string: For `conflict`, the prompt's wording now.
    - `reason` string: For `invalid`, `failed` and `conflict`: why, in a sentence you can act on.
    - `notes` string[]: Locations that could not be tracked as sent. A city that was not recognised is tracked country-wide; a state that matches no known state, and a US city found in more than one state, are not tracked. A city or state that could not be checked leaves the prompt's locations as they are, and the notes for those places are then the only ones the entry carries.

## Other responses

- `400`: Error response. `code` is the machine-readable reason — see the [error reference](/api/errors) for the full catalog and remediation per code.
- `401`: Error response. `code` is the machine-readable reason — see the [error reference](/api/errors) for the full catalog and remediation per code.
- `403`: Error response. `code` is the machine-readable reason — see the [error reference](/api/errors) for the full catalog and remediation per code.
- `404`: Error response. `code` is the machine-readable reason — see the [error reference](/api/errors) for the full catalog and remediation per code.
- `409`: Another request with the same `Idempotency-Key` is still executing. `Retry-After` (5 seconds) is always present — retry with the SAME key once it elapses.
- `429`: Rate limited. Usually the per-API-key REST limit (600 requests/minute); on Google-backed endpoints it can also relay an upstream Google rate limit. `Retry-After` (seconds) is present when the limit was applied by Searchable's own rate limiter; it may be absent when an upstream provider rate-limits.

## Changes

- **2026-11-03** `28a5e2a65cd3`: 1 info
  - endpoint added

[Operation history](https://skmtc.dev/searchable/apis/searchable-rest-api/changes/api/mcp/projects/:projectId/prompts/sync/post.md)

---

[API](https://skmtc.dev/searchable/apis/searchable-rest-api.md) - [All operations](https://skmtc.dev/searchable/apis/searchable-rest-api/llms.txt) - [OpenAPI document](https://skmtc.dev/searchable/apis/searchable-rest-api/revisions/a1cc70e46e73?raw)
