---
title: Re-Synthesize Opportunities
api: POST /brands/{brand_id}/synthesize
description: "Re-run opportunity synthesis and scoring from existing families + evidence, without re-crawling or re-calling DataForSEO."
---

> **For AI agents:** the complete documentation index is at [llms.txt](https://docs.boringmarketing.com/llms.txt). Append `.md` to any page URL for its markdown version.

Re-runs the opportunity engine synthesis layer on top of existing families and evidence. Does **not** re-crawl the brand, re-collect SERPs, or re-pull DataForSEO keywords — it reads everything from the database and re-runs the LLM synthesis + deterministic scoring.

**When to use this:** after updating brand context, changing competitors, tuning synthesis prompts, or when you want to refresh opportunity rankings without the cost of a full discovery.

**Duration:** ~30 seconds (vs ~14 minutes for full discovery).

## Path parameters

<ParamField path="brand_id" type="string" required>
  The brand UUID.
</ParamField>

## Request body

None.

## Response (202 Accepted)

```json
{
  "run_id": "run-uuid",
  "status": "queued"
}
```

The run is created with kind `synthesize`. Poll `GET /brands/{brand_id}/runs/{run_id}` until `status` is `done`, then fetch the refreshed opportunities via `GET /brands/{brand_id}/opportunities`.

## Example

```bash
curl -X POST \
  -H "X-API-Key: $BM_API_KEY" \
  https://api.boringmarketing.com/brands/$BRAND_ID/synthesize
```

<Note>
Synthesize reads families, keywords, and all evidence types (search, LLM, technical) from the database. If any of these are empty, run `POST /brands/{id}/discover` first. If you want to feed refreshed technical or LLM evidence into the synthesis, run the corresponding `/enrich/*` endpoint before synthesize.
</Note>

## Errors

| Status | Meaning |
|---|---|
| `404` | Brand not found, or not owned by the caller. |
| `409` | Returned when **(a)** another `synthesize` run is already active for this brand, or **(b)** a `full` (`/analyze`) run is currently writing to all tables. Other partial runs do **not** block this endpoint. Response carries an `X-Running-Run-Id` header with the existing run's UUID. Active runs older than 15 minutes are auto-expired before the conflict check. |
| `422` | `brand_id` is not a valid UUID. |
