Sync a quote with the trip's design
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
Undoable: the response carries Bymundi-Change-Id; revert it 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. Besides the refusals described above, any call like this one can return:
invalid_request— Invalid requestunauthorized— Missing or invalid API keyinsufficient_scope— Missing permissionrate_limited— Rate limit reachednot_found— Not foundidempotency_key_reused— Idempotency-Key reusedrequest_in_progress— Request in progress