# Sync a quote with the trip's design

> Does what the quote editor's Sync does: adds a priced line for each new journey and a hotel for each new stop of the trip's design, updates what the design owns on linked lines (routes, dates, times, cities, nights), and removes linked lines whose journey or stop is gone — only if nobody priced or edited them.

`POST /v1/quotes/{quoteId}/sync-design`

Does what the quote editor's Sync does: adds a priced line for each new journey and a hotel for each new stop of the trip's design, updates what the design owns on linked lines (routes, dates, times, cities, nights), and removes linked lines whose journey or stop is gone — only if nobody priced or edited them. Returns the counts. A trip with no design is a 422. With `?dryRun=true`, returns a `quote_preview` with the counts and the resulting lines.

**Permissions:** `quotes:write` · **Kind:** write · **Cost:** 1 unit · **Dry run:** `?dryRun=true` · MCP tool [`sync_quote_with_design`](https://api.bymundi.com/docs/mcp/tools/sync_quote_with_design.md)

Undoable: the response carries `Bymundi-Change-Id`; [revert it](https://api.bymundi.com/docs/guides/undo-and-dry-run.md) with `POST /v1/changes/{changeId}/revert`.

## Path parameters

| Field | Type | Required | Description |
|---|---|---|---|
| `quoteId` | uuid | yes |  |

## Body

| Field | Type | Required | Description |
|---|---|---|---|
| `expectedUpdatedAt` | datetime |  | The quote's updatedAt as you read it; the write is refused (409) if the quote changed since. |

## Response `200`

One of:

### `quote_sync`

| Field | Type | Required | Description |
|---|---|---|---|
| `object` | "quote_sync" | yes |  |
| `quote` | object | yes |  |
| `quote.object` | "quote" | yes |  |
| `quote.id` | uuid | yes |  |
| `quote.tripId` | uuid | yes |  |
| `quote.code` | string | yes |  |
| `quote.name` | string | yes |  |
| `quote.mode` | "package" \| "per_item" | yes |  |
| `quote.status` | "draft" \| "sent" \| "accepted" \| "rejected" \| "expired" \| "superseded" | yes |  |
| `quote.travelers` | integer | yes |  |
| `quote.marginPct` | number | yes |  |
| `quote.finalPriceOverride` | string \| null | yes |  |
| `quote.validUntil` | string \| null | yes |  |
| `quote.ctaUrl` | string \| null | yes |  |
| `quote.templateId` | string \| null | yes |  |
| `quote.packages` | object[] | yes |  |
| `quote.packages[].id` | string | yes |  |
| `quote.packages[].name` | string | yes |  |
| `quote.lines` | object \| object \| object[] | yes |  |
| `quote.totals` | object | yes |  |
| `quote.totals.currency` | "EUR" | yes |  |
| `quote.totals.net` | string | yes |  |
| `quote.totals.marginAmount` | string | yes |  |
| `quote.totals.total` | string | yes |  |
| `quote.totals.finalPrice` | string | yes |  |
| `quote.totals.perPax` | string | yes |  |
| `quote.totals.finalMargin` | string | yes |  |
| `quote.totals.packages` | object[] | yes |  |
| `quote.textOverrides` | object | yes |  |
| `quote.imageOverrides` | object | yes |  |
| `quote.sentAt` | string \| null | yes |  |
| `quote.modifiedSinceSent` | boolean | yes |  |
| `quote.acceptedAt` | string \| null | yes |  |
| `quote.acceptedVia` | "prospect_link" \| "agent" \| null | yes |  |
| `quote.acceptedPackageId` | string \| null | yes |  |
| `quote.acceptedLineIds` | string[] | yes |  |
| `quote.acceptance` | object \| null | yes | What the customer typed when accepting on the quote page — personal data: present only for a key holding `travelers:read`, only on quote reads (a write's response carries null; read the quote), and every such read is audited. |
| `quote.acceptance.name` | string \| null | yes |  |
| `quote.acceptance.email` | string \| null | yes |  |
| `quote.acceptance.phone` | string \| null | yes |  |
| `quote.acceptance.message` | string \| null | yes |  |
| `quote.engagement` | object | yes |  |
| `quote.engagement.views` | integer | yes |  |
| `quote.engagement.uniqueSessions` | integer | yes |  |
| `quote.engagement.activeSeconds` | integer | yes |  |
| `quote.engagement.ctaClicks` | integer | yes |  |
| `quote.engagement.maxScrollPct` | number | yes |  |
| `quote.engagement.sectionsSeen` | integer | yes |  |
| `quote.engagement.interactions` | integer | yes |  |
| `quote.engagement.lastSeenAt` | string \| null | yes |  |
| `quote.engagement.lastCtaAt` | string \| null | yes |  |
| `quote.metadata` | object | yes |  |
| `quote.appUrl` | string \| null | yes |  |
| `quote.publicUrl` | string \| null | yes |  |
| `quote.createdAt` | string | yes |  |
| `quote.updatedAt` | string | yes |  |
| `added` | integer | yes |  |
| `updated` | integer | yes |  |
| `removed` | integer | yes |  |

### `quote_preview`

| Field | Type | Required | Description |
|---|---|---|---|
| `object` | "quote_preview" | yes |  |
| `quoteId` | uuid \| null | yes |  |
| `changes` | object[] | yes |  |
| `changes[].field` | string | yes |  |
| `changes[].from` | any |  |  |
| `changes[].to` | any |  |  |
| `sync` | object \| null | yes |  |
| `sync.added` | integer | yes |  |
| `sync.updated` | integer | yes |  |
| `sync.removed` | integer | yes |  |
| `packages` | object[] | yes |  |
| `packages[].id` | string | yes |  |
| `packages[].name` | string | yes |  |
| `lines` | object \| object \| object[] | yes |  |
| `totals` | object | yes |  |
| `totals.currency` | "EUR" | yes |  |
| `totals.net` | string | yes |  |
| `totals.marginAmount` | string | yes |  |
| `totals.total` | string | yes |  |
| `totals.finalPrice` | string | yes |  |
| `totals.perPax` | string | yes |  |
| `totals.finalMargin` | string | yes |  |
| `totals.packages` | object[] | yes |  |
| `totals.packages[].id` | string | yes |  |
| `totals.packages[].name` | string | yes |  |
| `totals.packages[].deltaNet` | string | yes |  |
| `totals.packages[].deltaPvp` | string | yes |  |
| `totals.packages[].lineIds` | string[] | yes |  |


## Errors

Errors are [problem details](https://api.bymundi.com/docs/guides/errors.md). Besides the refusals described above, any call like this one can return:

- [`invalid_request`](https://api.bymundi.com/problems/invalid_request.md) — Invalid request
- [`unauthorized`](https://api.bymundi.com/problems/unauthorized.md) — Missing or invalid API key
- [`insufficient_scope`](https://api.bymundi.com/problems/insufficient_scope.md) — Missing permission
- [`rate_limited`](https://api.bymundi.com/problems/rate_limited.md) — Rate limit reached
- [`not_found`](https://api.bymundi.com/problems/not_found.md) — Not found
- [`idempotency_key_reused`](https://api.bymundi.com/problems/idempotency_key_reused.md) — Idempotency-Key reused
- [`request_in_progress`](https://api.bymundi.com/problems/request_in_progress.md) — Request in progress

## Examples

#### curl

```bash
curl -X POST https://api.bymundi.com/v1/quotes/$QUOTE_ID/sync-design \
  -H "Authorization: Bearer $BYMUNDI_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{}'
```

#### JavaScript

```javascript
const res = await fetch(`https://api.bymundi.com/v1/quotes/${quoteId}/sync-design`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.BYMUNDI_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": crypto.randomUUID(),
  },
  body: JSON.stringify({}),
});
if (!res.ok) throw new Error((await res.json()).detail);
const data = await res.json();
```

#### Python

```python
import os, uuid, requests

res = requests.post(
    f"https://api.bymundi.com/v1/quotes/{quoteId}/sync-design",
    headers={"Authorization": f"Bearer {os.environ['BYMUNDI_KEY']}", "Idempotency-Key": str(uuid.uuid4())},
    json={},
)
res.raise_for_status()
data = res.json()
```
