{"openapi":"3.1.0","info":{"title":"Plannyfy API Reference","version":"1.7.0","description":"REST API and HMAC-SHA256 webhook reference for Plannyfy. Use it to manage your own offerings, photos and collections, find opportunities and place bids, read earnings and payouts, manage and verify webhooks, read your payment methods and start escrow checkouts from your own systems.\n\n## Authentication\n\nSend an API key on every request: `Authorization: Bearer pfy_live_...` (or `X-Plannyfy-Api-Key: pfy_live_...`). Create keys in the Plannyfy app under Account > API keys. The secret is shown once, when you create the key; Plannyfy stores only a hash, so a lost key cannot be recovered, only replaced.\n\nA key only ever sees its own account's data. The vendor or customer the call acts for comes from the key, never from the request.\n\nKeys carry **scopes**. A call without the scope the endpoint needs gets `403` with code `insufficient_scope`. Each operation below lists its scope.\n\n## Rate limits and tiers\n\nEvery key has a tier. Defaults: **Free** 60 requests/minute and 5,000/day; **Pro** 600/minute and 100,000/day; **Enterprise** custom limits agreed with Plannyfy. Limits are per key; the day resets at 00:00 UTC. Every response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset` (Unix seconds); keys with a daily limit also get `X-RateLimit-Day-Limit` and `X-RateLimit-Day-Remaining`. When a limit is reached the API answers `429` with a `Retry-After` header (seconds). To move up a tier, contact concierge@plannyfy.com.\n\n## Conventions (1.6)\n\n**Version 1.7.0.** Additive changes (new endpoints, fields and error codes) do not change the major version `v1` in the path; breaking changes will ship under a new path version. See the changelog on the developers page.\n\n**Errors** always have one shape: `{ \"ok\": false, \"error\": { \"code\": \"...\", \"message\": \"...\", \"requestId\": \"req_...\" } }`. Branch on `code`. Every response, success or error, has an `X-Request-Id` header (equal to `error.requestId` on errors). Codes: `unauthenticated`, `invalid_key`, `key_revoked`, `key_expired`, `insufficient_scope`, `forbidden_role`, `session_only`, `cross_site`, `rate_limited`, `bad_request`, `validation_failed`, `not_found`, `conflict`, `payload_too_large`, `unsupported_media_type`, `idempotency_key_invalid`, `idempotency_key_reused`, `idempotency_in_progress`, `vendor_not_approved`, `bid_exists`, `bid_expired`, `trip_closed`, `trip_changed`, `confirmation_required`, `service_unavailable`, `internal_error`.\n\n**Idempotency.** Send `Idempotency-Key` (a UUID works well) on any POST to make it safe to retry: the first answer is kept 24 hours per key owner and replayed (header `Idempotent-Replayed: true`) for the same request; the same key with a different request is `409 idempotency_key_reused`.\n\n**Pagination.** List endpoints take `limit` and `cursor` and answer with `page: { limit, nextCursor, hasMore, total }` and a `next` link (null on the last page). Treat the cursor as opaque. A bad `limit` or `cursor` is `400`. Older list endpoints for photos and collections page with `page` and `limit` as documented on each.\n\n**Caching.** Every response is `Cache-Control: no-store`. There is no CORS: call the API from your servers, not from a browser on another site.\n\n**Request size.** JSON bodies up to 2 MB; photo uploads up to 25 MB.\n\n## Test keys\n\nKeys that start with `pfy_test_` belong to demo accounts and only work against demo data. They never touch real data.\n\nActions that are only available from the signed-in Plannyfy app (such as saving a payment method) cannot be done with a key and answer `403` with code `session_only`.","contact":{"name":"Plannyfy","email":"concierge@plannyfy.com"}},"servers":[{"url":"https://plannyfy.com","description":"Plannyfy"}],"tags":[{"name":"Portfolio & Inventory","description":"Query, upsert, and remove vendor portfolio offerings in Plannyfy Common Schema v1."},{"name":"Webhooks & Events","description":"Test and verify HMAC-SHA256 signed webhook deliveries for marketplace lifecycle events."},{"name":"Photos & Collections","description":"Your photo library, photo collections and the Plannyfy stock gallery you can add to a collection."},{"name":"Escrow & Billing","description":"Escrow checkout for your own trips and your saved payment methods."},{"name":"Opportunities & Bids","description":"Find the trips and events that match your services, place, revise, confirm or withdraw bids, link bid lines to your offerings, and sync availability."},{"name":"Earnings & Payouts","description":"Your won bookings, commission, net earnings and the state of each payout."},{"name":"Customer Trips & Concierge","description":"Create, edit, inspect, lock, complete, cancel and dispute your own trips, and exchange moderated concierge messages."}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"pfy_live_...","description":"API key (`Authorization: Bearer pfy_live_...`). Create keys in Account > API keys."},"apiKeyHeader":{"type":"apiKey","in":"header","name":"X-Plannyfy-Api-Key","description":"The same API key as a header (`X-Plannyfy-Api-Key: pfy_live_...`)."},"sessionCookie":{"type":"apiKey","in":"cookie","name":"pfy_token","description":"The signed-in Plannyfy app session. Used by Plannyfy's own screens; integrations should use an API key."}},"headers":{"X-RateLimit-Limit":{"description":"Requests allowed in the current window.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests left in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"When the window resets, in Unix seconds.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}},"X-Request-Id":{"description":"Id of this request. Quote it when you contact support. Equal to `error.requestId` on errors.","schema":{"type":"string","example":"req_3fa85f6457174562"}},"Idempotent-Replayed":{"description":"`true` when this answer is a stored replay of an earlier request with the same Idempotency-Key.","schema":{"type":"string","enum":["true"]}}},"responses":{"Unauthorized":{"description":"Missing, invalid, revoked or expired key (`unauthenticated`, `invalid_key`, `key_revoked`, `key_expired`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Forbidden":{"description":"The key lacks the scope (`insufficient_scope`), the account type cannot use the endpoint (`forbidden_role`) or the action is app-only (`session_only`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"RateLimited":{"description":"Rate limit reached (`rate_limited`). Retry after the `Retry-After` seconds.","headers":{"Retry-After":{"$ref":"#/components/headers/Retry-After"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"BadRequest":{"description":"Malformed request: bad JSON, an unknown query value, a bad `limit` or `cursor` (`bad_request`, `idempotency_key_invalid`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"NotFound":{"description":"Not found, or not yours: the same answer for both (`not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Conflict":{"description":"The request conflicts with the current state (`bid_exists`, `bid_expired`, `trip_closed`, `trip_changed`, `confirmation_required`, `idempotency_key_reused`, `idempotency_in_progress`, `conflict`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Unprocessable":{"description":"The request is well-formed but breaks a rule (`validation_failed`, `vendor_not_approved` is `403`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"TooLarge":{"description":"The body is too large (`payload_too_large`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"schemas":{"VendorPortfolioItemInput":{"type":"object","required":["title","basePriceUsd"],"properties":{"externalId":{"type":"string","description":"Partner SKU, room code, or external property identifier used for idempotent upserts.","example":"pal-roma-01"},"category":{"type":"string","enum":["hotel","activity","guide","transfer","train","flight","other"],"description":"Normalized travel & package category.","example":"hotel"},"title":{"type":"string","description":"Customer-facing offering title (max 140 chars).","example":"Hotel Palazzo Roma · 5★ Boutique Stay"},"description":{"type":"string","description":"Offering description, up to 4000 plain characters. Accepts plain text or a small HTML subset (p, br, strong, em, ul, ol, li, h2, h3, a with https href); anything else is stripped, as are phone numbers, emails and messaging links. Responses return sanitized HTML.","example":"Restored 17th-century palazzo steps from Piazza Navona with rooftop terrace & concierge."},"descriptionText":{"type":"string","readOnly":true,"description":"The description as plain text (returned on GET and POST responses; ignored on input).","example":"Restored 17th-century palazzo steps from Piazza Navona with rooftop terrace & concierge."},"location":{"type":"string","description":"City and country or region used for automatic trip leg matching.","example":"Rome, Italy"},"country":{"type":"string","example":"Italy"},"pricingUnit":{"type":"string","enum":["per_night","per_person","per_group","flat","per_hour","per_day","per_event","per_vehicle"],"description":"Unit used when multiplying base or seasonal rate across trip duration or party size.","example":"per_night"},"basePriceUsd":{"type":"number","description":"Standard net unit rate in USD (also accepts PMS aliases `price`, `rate`, `netRate`, `amountUsd`).","example":420},"retailReferenceUsd":{"type":"number","description":"Indicative public retail / OTA rack rate in USD (also accepts `retailPrice`, `rackRate`).","example":520},"seasonalRates":{"type":"object","description":"Optional seasonal rate tiers in USD automatically selected based on the customer's departure month (Jun–Aug = Peak, Apr–May & Sep–Oct = Shoulder, Nov–Mar = Low).","properties":{"peakSummerUsd":{"type":"number","example":510,"description":"Peak summer rate (Jun–Aug) in USD."},"shoulderUsd":{"type":"number","example":420,"description":"Shoulder season rate (Apr–May, Sep–Oct) in USD."},"lowSeasonUsd":{"type":"number","example":350,"description":"Low season rate (Nov–Mar) in USD."}}},"floorPctOfBase":{"type":"integer","minimum":60,"maximum":100,"description":"Private margin floor percentage (60–100) enforced by the Competition Slider and AI Bid Co-Pilot.","example":84},"maxCapacity":{"type":"integer","description":"Maximum guests supported per unit.","example":4},"photo":{"type":"string","enum":["rome","amalfi","kyoto","tokyo","swiss","alps","lisbon","bali","iceland","venice","santorini"],"description":"Curated Plannyfy destination photo key fallback.","example":"rome"},"galleryPhotos":{"type":"array","items":{"type":"string"},"description":"Curated Plannyfy destination photo keys for the multi-photo carousel.","example":["rome","venice","amalfi"]},"galleryUrls":{"type":"array","description":"Custom high-resolution image URLs (also accepts PMS aliases `images` or `photos`).","items":{"type":"string","format":"uri"},"example":["https://images.unsplash.com/photo-1552832230-c0197dd311b5?w=1200","https://images.unsplash.com/photo-1533105079780-92b9be482077?w=1200"]},"collectionId":{"type":"string","description":"Id of one of the vendor's photo collections (see `/api/v1/vendor/collections`). The offering links to it, so editing the collection updates the offering. In responses `galleryUrls` then lists the collection's photos, cover first. An id that is not the vendor's own is ignored.","example":"a1b2c3d4e5f60718293a"},"youtubeUrl":{"type":"string","format":"uri","description":"YouTube walkthrough URL (`youtube.com/watch?v=...` or `youtu.be/...`) embedded in the offer preview.","example":"https://www.youtube.com/watch?v=LXb3EKWsInQ"},"videoTitle":{"type":"string","description":"Caption for the embedded YouTube walkthrough.","example":"4K Palazzo Suite & Rooftop Terrace Walkthrough (60s)"},"socialReelUrl":{"type":"string","format":"uri","description":"Optional Instagram Reel or TikTok link.","example":"https://instagram.com/reel/atlasitalia-palazzo-roma"},"includedPerks":{"type":"array","description":"Included perks that boost Best-Value standing (also accepts `perks`, `inclusions`, `amenities`).","items":{"type":"string"},"example":["Daily breakfast included","Welcome Prosecco","14-day free cancellation"]},"highlights":{"type":"array","description":"Property, yacht, or tour highlights (also accepts `features`).","items":{"type":"string"},"example":["Rooftop Pantheon Terrace","24/7 Michelin Concierge","42m² Fresco Junior Suite"]},"tripAdvisorUrl":{"type":"string","format":"uri","description":"Verified TripAdvisor listing URL.","example":"https://www.tripadvisor.com/Hotel_Review-Palazzo_Roma"},"googleMapsUrl":{"type":"string","format":"uri","description":"Verified Google Maps place link.","example":"https://maps.google.com/?q=Piazza+Navona+Rome+Hotel+Palazzo"},"websiteUrl":{"type":"string","format":"uri","description":"Direct official property or experience webpage URL.","example":"https://atlasitalia.com/palazzo-roma"},"brochurePdfUrl":{"type":"string","format":"uri","description":"Downloadable PDF fact sheet or rate brochure URL.","example":"https://atlasitalia.com/brochures/palazzo-roma-factsheet.pdf"},"cancellationPolicy":{"type":"string","enum":["flexible_24h","moderate_7d","strict_14d","non_refundable"],"example":"strict_14d"},"autoMatchEnabled":{"type":"boolean","default":true,"description":"When true, automatically matches this offering to incoming trip requests in the same location and category."}}},"PortfolioItemInput":{"type":"object","required":["title","basePriceUsd"],"properties":{"externalId":{"type":"string","description":"Partner SKU, room code, or external property identifier used for idempotent upserts.","example":"pal-roma-01"},"category":{"type":"string","enum":["hotel","activity","guide","transfer","train","flight","other"],"description":"Normalized travel & package category.","example":"hotel"},"title":{"type":"string","description":"Customer-facing offering title (max 140 chars).","example":"Hotel Palazzo Roma · 5★ Boutique Stay"},"description":{"type":"string","description":"Offering description, up to 4000 plain characters. Accepts plain text or a small HTML subset (p, br, strong, em, ul, ol, li, h2, h3, a with https href); anything else is stripped, as are phone numbers, emails and messaging links. Responses return sanitized HTML.","example":"Restored 17th-century palazzo steps from Piazza Navona with rooftop terrace & concierge."},"descriptionText":{"type":"string","readOnly":true,"description":"The description as plain text (returned on GET and POST responses; ignored on input).","example":"Restored 17th-century palazzo steps from Piazza Navona with rooftop terrace & concierge."},"location":{"type":"string","description":"City and country or region used for automatic trip leg matching.","example":"Rome, Italy"},"country":{"type":"string","example":"Italy"},"pricingUnit":{"type":"string","enum":["per_night","per_person","per_group","flat","per_hour","per_day","per_event","per_vehicle"],"description":"Unit used when multiplying base or seasonal rate across trip duration or party size.","example":"per_night"},"basePriceUsd":{"type":"number","description":"Standard net unit rate in USD (also accepts PMS aliases `price`, `rate`, `netRate`, `amountUsd`).","example":420},"retailReferenceUsd":{"type":"number","description":"Indicative public retail / OTA rack rate in USD (also accepts `retailPrice`, `rackRate`).","example":520},"seasonalRates":{"type":"object","description":"Optional seasonal rate tiers in USD automatically selected based on the customer's departure month (Jun–Aug = Peak, Apr–May & Sep–Oct = Shoulder, Nov–Mar = Low).","properties":{"peakSummerUsd":{"type":"number","example":510,"description":"Peak summer rate (Jun–Aug) in USD."},"shoulderUsd":{"type":"number","example":420,"description":"Shoulder season rate (Apr–May, Sep–Oct) in USD."},"lowSeasonUsd":{"type":"number","example":350,"description":"Low season rate (Nov–Mar) in USD."}}},"floorPctOfBase":{"type":"integer","minimum":60,"maximum":100,"description":"Private margin floor percentage (60–100) enforced by the Competition Slider and AI Bid Co-Pilot.","example":84},"maxCapacity":{"type":"integer","description":"Maximum guests supported per unit.","example":4},"photo":{"type":"string","enum":["rome","amalfi","kyoto","tokyo","swiss","alps","lisbon","bali","iceland","venice","santorini"],"description":"Curated Plannyfy destination photo key fallback.","example":"rome"},"galleryPhotos":{"type":"array","items":{"type":"string"},"description":"Curated Plannyfy destination photo keys for the multi-photo carousel.","example":["rome","venice","amalfi"]},"galleryUrls":{"type":"array","description":"Custom high-resolution image URLs (also accepts PMS aliases `images` or `photos`).","items":{"type":"string","format":"uri"},"example":["https://images.unsplash.com/photo-1552832230-c0197dd311b5?w=1200","https://images.unsplash.com/photo-1533105079780-92b9be482077?w=1200"]},"collectionId":{"type":"string","description":"Id of one of the vendor's photo collections (see `/api/v1/vendor/collections`). The offering links to it, so editing the collection updates the offering. In responses `galleryUrls` then lists the collection's photos, cover first. An id that is not the vendor's own is ignored.","example":"a1b2c3d4e5f60718293a"},"youtubeUrl":{"type":"string","format":"uri","description":"YouTube walkthrough URL (`youtube.com/watch?v=...` or `youtu.be/...`) embedded in the offer preview.","example":"https://www.youtube.com/watch?v=LXb3EKWsInQ"},"videoTitle":{"type":"string","description":"Caption for the embedded YouTube walkthrough.","example":"4K Palazzo Suite & Rooftop Terrace Walkthrough (60s)"},"socialReelUrl":{"type":"string","format":"uri","description":"Optional Instagram Reel or TikTok link.","example":"https://instagram.com/reel/atlasitalia-palazzo-roma"},"includedPerks":{"type":"array","description":"Included perks that boost Best-Value standing (also accepts `perks`, `inclusions`, `amenities`).","items":{"type":"string"},"example":["Daily breakfast included","Welcome Prosecco","14-day free cancellation"]},"highlights":{"type":"array","description":"Property, yacht, or tour highlights (also accepts `features`).","items":{"type":"string"},"example":["Rooftop Pantheon Terrace","24/7 Michelin Concierge","42m² Fresco Junior Suite"]},"tripAdvisorUrl":{"type":"string","format":"uri","description":"Verified TripAdvisor listing URL.","example":"https://www.tripadvisor.com/Hotel_Review-Palazzo_Roma"},"googleMapsUrl":{"type":"string","format":"uri","description":"Verified Google Maps place link.","example":"https://maps.google.com/?q=Piazza+Navona+Rome+Hotel+Palazzo"},"websiteUrl":{"type":"string","format":"uri","description":"Direct official property or experience webpage URL.","example":"https://atlasitalia.com/palazzo-roma"},"brochurePdfUrl":{"type":"string","format":"uri","description":"Downloadable PDF fact sheet or rate brochure URL.","example":"https://atlasitalia.com/brochures/palazzo-roma-factsheet.pdf"},"cancellationPolicy":{"type":"string","enum":["flexible_24h","moderate_7d","strict_14d","non_refundable"],"example":"strict_14d"},"autoMatchEnabled":{"type":"boolean","default":true,"description":"When true, automatically matches this offering to incoming trip requests in the same location and category."}}},"Error":{"type":"object","required":["ok","error"],"description":"Every error answer has this shape, with the `X-Request-Id` header equal to `error.requestId`. Some errors add fields next to `error` (for example `needsConfirm`, `bidId`, `errors`).","properties":{"ok":{"type":"boolean","enum":[false]},"error":{"type":"object","required":["code","message","requestId"],"properties":{"code":{"type":"string","example":"insufficient_scope","description":"Stable, machine-readable. Branch on this, never on `message`."},"message":{"type":"string","example":"This API key needs the `bids:write` scope."},"requestId":{"type":"string","example":"req_3fa85f6457174562","description":"Quote this when you contact support."}}}}},"Page":{"type":"object","required":["limit","nextCursor","hasMore","total"],"properties":{"limit":{"type":"integer","example":50},"nextCursor":{"type":["string","null"]},"hasMore":{"type":"boolean"},"total":{"type":"integer","description":"How many items match the filters in all pages."}}},"BidLineInput":{"type":"object","required":["itemId"],"description":"One priced line. Send `price` (USD, whole dollars), or for event items priced by unit send `unit` with `unitPrice` (and `minGuests` on per-guest lines) and the server computes `price`. Prices more than 50% above the market reference are refused.","properties":{"itemId":{"type":"string","description":"A trip item id from the opportunity."},"price":{"type":"number","example":1680},"unit":{"type":"string","enum":["per_night","per_person","per_group","flat","per_hour","per_day","per_event","per_vehicle"]},"unitPrice":{"type":"number","example":85},"minGuests":{"type":"integer","minimum":1,"description":"Per-guest lines: fewer guests are billed as this many."},"inclusions":{"type":"string","maxLength":200,"example":"Daily breakfast, airport transfer"},"offeringId":{"type":["string","null"],"description":"Link the line to one of your own offerings (see `offeringCandidates` on the opportunity). The server verifies it is yours and rebuilds the snapshot; any other id is refused with one generic message. `null` detaches. Leave it out on a revise to keep the stored link."},"tierId":{"type":"string","description":"A rate choice (room type or tier) on the linked offering."},"acknowledgeCrossCategory":{"type":"boolean","description":"Confirm linking an offering from a different category."}}},"BidLine":{"type":"object","properties":{"itemId":{"type":"string"},"price":{"type":"number"},"inclusions":{"type":"string"},"unit":{"type":"string"},"unitPrice":{"type":"number"},"minGuests":{"type":"integer"},"offering":{"type":"object","description":"The verified offering link: `id`, `source` (auto or manual), `matchedAt`, `edited`, `syncedAt` and a frozen `snapshot` (title, category, basis rate, unit, inclusions, location, photos, cancellation policy). Never wholesale rates or markup."}}},"Bid":{"type":"object","properties":{"id":{"type":"string"},"tripId":{"type":"string"},"state":{"type":"string","enum":["live","past"]},"status":{"type":"string","description":"Live bids: the standing label. Past bids: `won`, `not_selected`, `withdrawn`, `expired` or `trip_cancelled`."},"settledAt":{"type":"string","format":"date-time"},"kind":{"type":"string","enum":["itemized","bundle"]},"total":{"type":"number"},"lines":{"type":"array","items":{"$ref":"#/components/schemas/BidLine"}},"validUntil":{"type":"string","format":"date-time"},"createdAt":{"type":"string","format":"date-time"},"floorPrice":{"type":"number","description":"Your own private floor. Never shown to travelers or other vendors."},"tripRevision":{"type":"integer"},"needsReprice":{"type":"object","description":"Present while the bid is paused after the traveler changed items it priced."},"draft":{"type":"object","description":"Your saved, unpublished edit."},"revisions":{"type":"array","items":{"type":"object"}},"health":{"type":"object","properties":{"band":{"type":"string"},"position":{"type":"string"},"hoursRemaining":{"type":"number"},"expiringSoon":{"type":"boolean"}},"description":"Live bids: where the bid stands, as a band. No rival price, no exact gap."},"trip":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"destination":{"type":"string"},"startDate":{"type":"string"},"endDate":{"type":"string"},"biddingClosesAt":{"type":"string"},"packageType":{"type":"string"},"guestCount":{"type":"integer"},"revision":{"type":"integer"}}}}},"EventDetails":{"type":"object","description":"Sanitized event details. Never the customer's invoice note, and never the venue's street address, coordinates or place id (city, region and country only).","properties":{"subtype":{"type":"string","example":"wedding"},"eventDate":{"type":"string","format":"date"},"dateFlexDays":{"type":"integer"},"venueStatus":{"type":"string","enum":["booked","searching"]},"venueLocation":{"type":"object","properties":{"city":{"type":"string"},"region":{"type":"string"},"country":{"type":"string"}}},"guestRange":{"type":"object","properties":{"min":{"type":"integer"},"max":{"type":"integer"}}},"durationHours":{"type":"integer"},"indoorOutdoor":{"type":"string","enum":["indoor","outdoor","both"]},"style":{"type":"string"},"sameVenue":{"type":"boolean"},"guestsTravelIn":{"type":"boolean"},"overnightStay":{"type":"boolean"},"avNeeds":{"type":"string","enum":["none","basic","full"]}}},"Opportunity":{"type":"object","description":"An open trip or event you can bid on. Traveler-safe: a market benchmark and a competition band, never the traveler's budget, a target price or a rival's price.","properties":{"id":{"type":"string"},"packageType":{"type":"string","example":"travel"},"title":{"type":"string"},"destination":{"type":"string"},"startDate":{"type":"string"},"endDate":{"type":"string"},"guestCount":{"type":"integer","description":"Guests (events) or travelers."},"travelers":{"type":"integer"},"priority":{"type":"string","enum":["price","value","balanced"]},"biddingClosesAt":{"type":"string","format":"date-time"},"closesInHours":{"type":"number"},"publishedAt":{"type":"string","format":"date-time"},"revision":{"type":"integer","description":"Changes on every material edit by the traveler. Send it back as `tripRevision` when you bid."},"marketTotalUsd":{"type":"number"},"competition":{"type":"string","description":"A band, not a bid count."},"alreadyBid":{"type":"boolean"},"matchesServices":{"type":"boolean"},"matchScore":{"type":"integer"},"leadTimeDays":{"type":"integer"},"tags":{"type":"array","items":{"type":"string"}},"venueCity":{"type":"string"},"eventDetails":{"$ref":"#/components/schemas/EventDetails"},"changes":{"type":"array","items":{"type":"object","properties":{"at":{"type":"string"},"summary":{"type":"string"}}}},"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"category":{"type":"string"},"title":{"type":"string"},"details":{"type":"string"},"location":{"type":"string"},"quantity":{"type":"integer"},"unit":{"type":"string","description":"Events: what the quantity is priced in."},"perGuest":{"type":"boolean"},"startDate":{"type":"string"},"endDate":{"type":"string"},"specs":{"type":"object","additionalProperties":{"type":"string"}},"flexDays":{"type":"integer"},"referencePriceUsd":{"type":"number","description":"The public market benchmark, not the traveler's target."}}}}}},"Payout":{"type":"object","properties":{"id":{"type":"string"},"tripId":{"type":"string"},"tripTitle":{"type":"string"},"status":{"type":"string","enum":["pending","eligible","awaiting_approval","processing","paid","failed","held","reversed","cancelled"]},"label":{"type":"string","example":"Paid"},"note":{"type":"string","description":"One sentence explaining the state, as shown in the app."},"needsBilling":{"type":"boolean","description":"True when you can fix the cause yourself in Billing."},"originalGrossUsd":{"type":"number"},"refundedGrossUsd":{"type":"number"},"grossUsd":{"type":"number"},"commissionUsd":{"type":"number"},"netUsd":{"type":"number"},"transferredUsd":{"type":"number"},"reversedUsd":{"type":"number"},"eligibleAt":{"type":"string","format":"date-time"},"paidAt":{"type":"string","format":"date-time"},"transferId":{"type":"string","description":"Only once paid."},"lines":{"type":"array","items":{"type":"object"}},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"WebhookSettings":{"type":"object","properties":{"url":{"type":["string","null"],"format":"uri"},"events":{"type":"array","items":{"type":"string","enum":["opportunity.matched","bid.status_changed","trip.locked","dispute.updated"]}},"secretHint":{"type":["string","null"],"example":"whsec_…9c1d","description":"The secret itself is never returned except once, by rotate-secret."},"lastTestAt":{"type":["string","null"]},"lastTestStatus":{"type":["string","null"]},"updatedAt":{"type":"string"}}},"WebhookEnvelope":{"type":"object","required":["id","version","event","vendorId","createdAt","data"],"description":"Every webhook body. Use `id` to de-duplicate: the same event may in rare cases be delivered more than once.","properties":{"id":{"type":"string","example":"evt_mh3k2x9a_1f2e3d4c"},"version":{"type":"string","example":"v1"},"event":{"type":"string","enum":["opportunity.matched","bid.status_changed","trip.locked","dispute.updated"]},"vendorId":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"data":{"type":"object"}}}},"parameters":{"IdempotencyKey":{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","minLength":1,"maxLength":255,"example":"7f1b8f0e-1d1c-4c4e-8a77-0a4f6a1f5c2e"},"description":"Makes a POST safe to retry. The first request with a key runs and its answer is kept for 24 hours, per key owner. The same key with the same method, path and body returns that answer again with `Idempotent-Replayed: true` and does not run again. The same key with anything different is `409 idempotency_key_reused`; while the first request is still running it is `409 idempotency_in_progress`. 5xx and 429 answers are not kept, so you can retry them."},"Limit":{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":50},"description":"Page size."},"Cursor":{"name":"cursor","in":"query","required":false,"schema":{"type":"string"},"description":"Opaque position from `page.nextCursor` of the previous response."}}},"paths":{"/api/v1/vendor/portfolio":{"get":{"tags":["Portfolio & Inventory"],"summary":"List vendor portfolio offerings","description":"Returns all portfolio items for the authenticated vendor in Plannyfy Common Schema v1 (`plannyfy.travel.portfolio.v1`), optionally filtered by category. Required scope: `portfolio:read` (Read your offerings.) Accounts: vendor.","security":[{"bearerAuth":["portfolio:read"]},{"apiKeyHeader":[]},{"sessionCookie":[]}],"parameters":[{"name":"category","in":"query","required":false,"schema":{"type":"string","enum":["hotel","activity","guide","transfer","train","flight","other"]},"description":"Optional category filter."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":500,"default":200},"description":"Page size."},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"},"description":"Opaque position from `page.nextCursor` of the previous response."}],"responses":{"200":{"description":"Normalized portfolio items list: `items`, `count` (in this page), `total`, `page` and a `next` link while more remain.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"},"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}},"x-required-scope":"portfolio:read"},"post":{"tags":["Portfolio & Inventory"],"summary":"Upsert 1–100 portfolio offerings","description":"Upserts between 1 and 100 portfolio items per request. Accepts canonical Plannyfy Schema v1 (`{ items: [...] }`, array, or single object) as well as external PMS/OTA field aliases (`name`, `rate`, `sku`, `perks`, `images`). Required scope: `portfolio:write` (Create, update and delete your offerings.) Accounts: vendor.","security":[{"bearerAuth":["portfolio:write"]},{"apiKeyHeader":[]},{"sessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/VendorPortfolioItemInput"},{"type":"object","required":["items"],"properties":{"version":{"type":"string","example":"v1"},"items":{"type":"array","maxItems":100,"items":{"$ref":"#/components/schemas/VendorPortfolioItemInput"}}}}]}}}},"responses":{"200":{"description":"Portfolio items validated and upserted (`upsertedCount`, `items`).","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"},"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"Idempotent-Replayed":{"$ref":"#/components/headers/Idempotent-Replayed"}}},"400":{"description":"Validation error in payload."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}},"x-required-scope":"portfolio:write","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}]},"delete":{"tags":["Portfolio & Inventory"],"summary":"Delete a portfolio offering","description":"Removes a portfolio offering by Plannyfy `id` or partner `externalId` (SKU). Required scope: `portfolio:write` (Create, update and delete your offerings.) Accounts: vendor.","security":[{"bearerAuth":["portfolio:write"]},{"apiKeyHeader":[]},{"sessionCookie":[]}],"parameters":[{"name":"id","in":"query","required":false,"schema":{"type":"string"},"description":"Plannyfy portfolio item ID (e.g. `pf-rome-palazzo`)."},{"name":"externalId","in":"query","required":false,"schema":{"type":"string"},"description":"Partner external SKU / code (e.g. `pal-roma-01`)."}],"responses":{"200":{"description":"Item deleted (`deletedId`).","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"},"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"}}},"400":{"description":"Missing `id` or `externalId` query parameter."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}},"x-required-scope":"portfolio:write"}},"/api/v1/vendor/sub-vendors":{"get":{"tags":["Portfolio & Inventory"],"summary":"List sub-vendor invitations & linked wholesale partners","description":"Returns all sub-vendor invitations (`pending`, `accepted`, `revoked`), wholesale markup rules, and aggregated sub-vendor portfolio offerings for the primary agency/DMC. Required scope: `subvendors:read` (Read your sub-vendor invites and synced offerings.) Accounts: vendor.","security":[{"bearerAuth":["subvendors:read"]},{"apiKeyHeader":[]},{"sessionCookie":[]}],"responses":{"200":{"description":"Primary vendor's `subVendorInvites`, linked sub-vendors, and wholesale-marked-up portfolio items.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"},"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}},"x-required-scope":"subvendors:read"},"post":{"tags":["Portfolio & Inventory"],"summary":"Create a sub-vendor invitation or accept a sub-vendor onboarding token","description":"Issues a scoped sub-vendor invitation link (`/vendor/invite/[token]`) with allowed categories and default wholesale markup percentage, or accepts an invitation token to link a portfolio-only sub-vendor to the parent vendor. Required scope: `subvendors:write` (Push sub-vendor offerings and manage invites.) Accounts: vendor.","security":[{"bearerAuth":["subvendors:write"]},{"apiKeyHeader":[]},{"sessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"action":{"type":"string","enum":["invite","accept","revoke"],"default":"invite"},"inviteeEmail":{"type":"string","format":"email","example":"concierge@villacimbrone-charter.it"},"inviteeBusinessName":{"type":"string","example":"Amalfi Riva Yacht & Helicopter Charter"},"vendorType":{"type":"string","enum":["hotel","tour_operator","guide","transfer","agency","flight_consolidator"],"default":"tour_operator"},"allowedCategories":{"type":"array","items":{"type":"string","enum":["hotel","activity","guide","transfer","train","flight","other"]}},"defaultMarkupPct":{"type":"number","minimum":0,"maximum":50,"example":15},"token":{"type":"string","description":"Sub-vendor invitation token when `action` is `accept` or `revoke`."}}}}}},"responses":{"200":{"description":"Created or updated sub-vendor invitation record and shareable `/vendor/invite/[token]` URL.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"},"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"Idempotent-Replayed":{"$ref":"#/components/headers/Idempotent-Replayed"}}},"400":{"description":"Invalid sub-vendor invitation parameters."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}},"x-required-scope":"subvendors:write","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}]}},"/api/v1/vendor/webhooks/test":{"post":{"tags":["Webhooks & Events"],"summary":"Trigger a signed test webhook event","description":"Generates a sample webhook payload (`opportunity.matched`, `bid.status_changed`, `trip.locked`, or `dispute.updated`), signs it with the vendor's `webhookSecret` using HMAC-SHA256 (`X-Plannyfy-Signature: t=<unix_sec>,v1=<hex>`), dispatches it to the configured webhook URL (or to the `webhookUrl` in the request body, which must be a public https URL and is saved as your endpoint), and records the delivery log. Required scope: `webhooks:test` (Send a signed test webhook to your endpoint.) Accounts: vendor.","security":[{"bearerAuth":["webhooks:test"]},{"apiKeyHeader":[]},{"sessionCookie":[]}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"event":{"type":"string","enum":["opportunity.matched","bid.status_changed","trip.locked","dispute.updated"],"default":"opportunity.matched"},"webhookUrl":{"type":"string","format":"uri","description":"Optional override destination URL for testing."}}}}}},"responses":{"200":{"description":"Signed webhook payload, `signatureHeader`, HTTP delivery status, and delivery log entry.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"},"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"Idempotent-Replayed":{"$ref":"#/components/headers/Idempotent-Replayed"}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}},"x-required-scope":"webhooks:test","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}]}},"/api/v1/vendor/photos":{"get":{"tags":["Photos & Collections"],"summary":"List the vendor's photo library","description":"Returns the signed-in vendor's uploaded photos (newest first by default), filtered by search text (alt text, file name, tags) and tags. Without `page` or `limit` every match is returned. Session or ID-token auth only. Required scope: `photos:read` (Read your photo library and the Plannyfy gallery.) Accounts: vendor.","security":[{"bearerAuth":["photos:read"]},{"apiKeyHeader":[]},{"sessionCookie":[]}],"parameters":[{"name":"q","in":"query","schema":{"type":"string"},"description":"Search text."},{"name":"tag","in":"query","schema":{"type":"string"},"description":"Filter by tag. Repeat or comma-separate; an item must carry every tag."},{"name":"page","in":"query","schema":{"type":"integer","minimum":1},"description":"Page number. Paging is on when `page` or `limit` is sent."},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100},"description":"Page size (default 24 when paging)."},{"name":"sort","in":"query","schema":{"type":"string","enum":["newest","oldest","name"]}}],"responses":{"200":{"description":"`photos`: array of { id, url, thumbUrl, width, height, bytes, alt, tags, fileName, createdAt, source }, plus `total`, `totalAll`, `page`, `pageSize`, `pageCount` and `tags` (every tag in the library with its `count`).","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"},"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}},"x-required-scope":"photos:read"},"post":{"tags":["Photos & Collections"],"summary":"Upload a photo","description":"multipart/form-data with one `file` and an optional `tags` field (comma-separated) applied to the photo (JPG, PNG, WebP or HEIC, up to 25 MB; the type is checked from the file's bytes). The server converts it to webp (2000px long edge) plus a 400px thumbnail and strips EXIF and GPS data. Up to 500 photos per vendor and 30 uploads per minute. Required scope: `photos:write` (Upload, edit and delete your photos.) Accounts: vendor.","security":[{"bearerAuth":["photos:write"]},{"apiKeyHeader":[]},{"sessionCookie":[]}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["file"],"properties":{"file":{"type":"string","format":"binary"},"tags":{"type":"string","description":"Comma-separated tags, up to 10."}}}}}},"responses":{"201":{"description":"The stored photo.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"},"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"Idempotent-Replayed":{"$ref":"#/components/headers/Idempotent-Replayed"}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"description":"`cap_reached`: the library already holds 500 photos."},"413":{"description":"`too_large`: over 25 MB."},"415":{"description":"`not_an_image`: not a JPG, PNG, WebP or HEIC image."},"422":{"description":"`unreadable`: the image could not be decoded."},"429":{"$ref":"#/components/responses/RateLimited"}},"x-required-scope":"photos:write","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}]}},"/api/v1/vendor/photos/bulk":{"post":{"tags":["Photos & Collections"],"summary":"Change several photos at once","description":"Applies one action to up to 100 of the vendor's own photos and answers 200 with a result per photo: `done`, `unchanged` (nothing to do, so repeating a request is safe) or `failed` with a `code` (`not_found`, `in_use`, `cap_reached`, `tag_limit`, `collection_not_found`). One failure never blocks the others. Deleting keeps photos that offerings still show (`in_use`) unless `force` is true. 30 requests per minute. Required scope: `photos:write` (Upload, edit and delete your photos.) Accounts: vendor.","security":[{"bearerAuth":["photos:write"]},{"apiKeyHeader":[]},{"sessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["action","ids"],"properties":{"action":{"type":"string","enum":["add_tags","remove_tags","add_to_collection","delete"]},"ids":{"type":"array","maxItems":100,"items":{"type":"string"}},"tags":{"type":"array","maxItems":10,"items":{"type":"string","maxLength":24},"description":"Free tags: lowercase letters, digits and hyphens, up to 10 of up to 24 characters. Text is slugged (\"Rooftop Suite\" becomes \"rooftop-suite\"), duplicates are dropped."},"collectionId":{"type":"string","description":"For `add_to_collection`."},"force":{"type":"boolean","description":"For `delete`: also delete photos that offerings show."}}}}}},"responses":{"200":{"description":"`results` (one per id) and `summary` { done, unchanged, failed }.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"},"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"Idempotent-Replayed":{"$ref":"#/components/headers/Idempotent-Replayed"}}},"400":{"description":"Invalid action, ids or tags."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}},"x-required-scope":"photos:write","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}]}},"/api/v1/vendor/photos/{id}":{"patch":{"tags":["Photos & Collections"],"summary":"Edit a photo's alt text and tags","description":"Send `alt`, `tags` or both; what is not sent stays as it is. `tags` replaces the photo's tags. Required scope: `photos:write` (Upload, edit and delete your photos.) Accounts: vendor.","security":[{"bearerAuth":["photos:write"]},{"apiKeyHeader":[]},{"sessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"alt":{"type":"string","maxLength":200},"tags":{"type":"array","maxItems":10,"items":{"type":"string","maxLength":24},"description":"Free tags: lowercase letters, digits and hyphens, up to 10 of up to 24 characters. Text is slugged (\"Rooftop Suite\" becomes \"rooftop-suite\"), duplicates are dropped."}}}}}},"responses":{"200":{"description":"The updated photo.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"},"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"}}},"400":{"description":"Invalid alt text or tags."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Not found (or not yours)."},"429":{"$ref":"#/components/responses/RateLimited"}},"x-required-scope":"photos:write"},"delete":{"tags":["Photos & Collections"],"summary":"Delete a photo","description":"Answers 409 `in_use` with `usedBy` while offerings still show the photo. Add `?force=1` to delete anyway; the photo is then removed from those offerings. Required scope: `photos:write` (Upload, edit and delete your photos.) Accounts: vendor.","security":[{"bearerAuth":["photos:write"]},{"apiKeyHeader":[]},{"sessionCookie":[]}],"responses":{"200":{"description":"Deleted.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"},"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Not found (or not yours)."},"409":{"description":"`in_use`."},"429":{"$ref":"#/components/responses/RateLimited"}},"x-required-scope":"photos:write"}},"/api/v1/vendor/gallery":{"get":{"tags":["Photos & Collections"],"summary":"List the Plannyfy gallery","description":"Returns the active Plannyfy gallery (stock) photos a vendor can add to a collection as `{ kind: \"plannyfy\", key }`. Each has `key`, `title`, `alt`, `url`, `thumbUrl`, `destinations`, `categories` (filter facets) and `tags` (free tags). `collections` lists the active Plannyfy collections (curated sets such as \"Amalfi Coast hotels\") with `id`, `name`, `description`, `tags`, `coverThumbUrl`, `count` and their visible `photoKeys`: add them all as `plannyfy` items to put the whole set in a collection. Travelers see these photos marked as illustrative. Session or ID-token auth only. Required scope: `photos:read` (Read your photo library and the Plannyfy gallery.) Accounts: vendor.","security":[{"bearerAuth":["photos:read"]},{"apiKeyHeader":[]},{"sessionCookie":[]}],"responses":{"200":{"description":"`photos` and `collections`.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"},"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}},"x-required-scope":"photos:read"}},"/api/v1/vendor/collections":{"get":{"tags":["Photos & Collections"],"summary":"List photo collections","description":"Returns the signed-in vendor's collections. Each entry has `collection` (with `tags`), `photos` (resolved urls, cover first) and `usedBy` (the offerings that link to it). Filter by name or tag; without `page` or `limit` every match is returned. Session or ID-token auth only. Required scope: `collections:read` (Read your photo collections.) Accounts: vendor.","security":[{"bearerAuth":["collections:read"]},{"apiKeyHeader":[]},{"sessionCookie":[]}],"parameters":[{"name":"q","in":"query","schema":{"type":"string"},"description":"Search text."},{"name":"tag","in":"query","schema":{"type":"string"},"description":"Filter by tag. Repeat or comma-separate; an item must carry every tag."},{"name":"page","in":"query","schema":{"type":"integer","minimum":1},"description":"Page number. Paging is on when `page` or `limit` is sent."},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100},"description":"Page size (default 24 when paging)."},{"name":"sort","in":"query","schema":{"type":"string","enum":["updated","name","photos"]}}],"responses":{"200":{"description":"`collections`, plus `total`, `totalAll`, `page`, `pageSize`, `pageCount` and `tags` (every tag in use with its `count`).","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"},"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}},"x-required-scope":"collections:read"},"post":{"tags":["Photos & Collections"],"summary":"Create a photo collection","description":"Up to 30 photos and 100 collections per vendor. Library photos must be the vendor's own. Alternatively send `{ fromOfferingId, name? }` to save an offering's current photos as a collection and link the offering to it. Required scope: `collections:write` (Create, edit, duplicate and delete your photo collections.) Accounts: vendor.","security":[{"bearerAuth":["collections:write"]},{"apiKeyHeader":[]},{"sessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","items"],"properties":{"name":{"type":"string","maxLength":60},"tags":{"type":"array","maxItems":10,"items":{"type":"string","maxLength":24},"description":"Free tags: lowercase letters, digits and hyphens, up to 10 of up to 24 characters. Text is slugged (\"Rooftop Suite\" becomes \"rooftop-suite\"), duplicates are dropped."},"items":{"type":"array","maxItems":30,"items":{"type":"object","properties":{"ref":{"type":"object","properties":{"kind":{"type":"string","enum":["library","plannyfy"]},"photoId":{"type":"string","description":"For `library`."},"key":{"type":"string","description":"For `plannyfy` (a Plannyfy gallery photo, used by reference and shown to travelers as illustrative)."}}},"alt":{"type":"string","maxLength":200},"caption":{"type":"string","maxLength":160}}}},"coverPhotoRef":{"type":"object","properties":{"kind":{"type":"string","enum":["library","plannyfy"]},"photoId":{"type":"string","description":"For `library`."},"key":{"type":"string","description":"For `plannyfy` (a Plannyfy gallery photo, used by reference and shown to travelers as illustrative)."}}}}}}}},"responses":{"201":{"description":"The collection view.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"},"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"Idempotent-Replayed":{"$ref":"#/components/headers/Idempotent-Replayed"}}},"400":{"description":"Invalid name, photo or reference."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"description":"`cap_reached`."},"429":{"$ref":"#/components/responses/RateLimited"}},"x-required-scope":"collections:write","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}]}},"/api/v1/vendor/collections/{id}":{"get":{"tags":["Photos & Collections"],"summary":"Get a photo collection","security":[{"bearerAuth":["collections:read"]},{"apiKeyHeader":[]},{"sessionCookie":[]}],"responses":{"200":{"description":"The collection view.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"},"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Not found (or not yours)."},"429":{"$ref":"#/components/responses/RateLimited"}},"x-required-scope":"collections:read","description":"Required scope: `collections:read` (Read your photo collections.) Accounts: vendor."},"patch":{"tags":["Photos & Collections"],"summary":"Edit a photo collection","description":"Replaces the name, photos, order and cover. Every offering linked to the collection shows the change. Required scope: `collections:write` (Create, edit, duplicate and delete your photo collections.) Accounts: vendor.","security":[{"bearerAuth":["collections:write"]},{"apiKeyHeader":[]},{"sessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","items"],"properties":{"name":{"type":"string","maxLength":60},"tags":{"type":"array","maxItems":10,"items":{"type":"string","maxLength":24},"description":"Free tags: lowercase letters, digits and hyphens, up to 10 of up to 24 characters. Text is slugged (\"Rooftop Suite\" becomes \"rooftop-suite\"), duplicates are dropped."},"items":{"type":"array","maxItems":30,"items":{"type":"object","properties":{"ref":{"type":"object","properties":{"kind":{"type":"string","enum":["library","plannyfy"]},"photoId":{"type":"string","description":"For `library`."},"key":{"type":"string","description":"For `plannyfy` (a Plannyfy gallery photo, used by reference and shown to travelers as illustrative)."}}},"alt":{"type":"string","maxLength":200},"caption":{"type":"string","maxLength":160}}}},"coverPhotoRef":{"type":"object","properties":{"kind":{"type":"string","enum":["library","plannyfy"]},"photoId":{"type":"string","description":"For `library`."},"key":{"type":"string","description":"For `plannyfy` (a Plannyfy gallery photo, used by reference and shown to travelers as illustrative)."}}}}}}}},"responses":{"200":{"description":"The updated collection view.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"},"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"}}},"400":{"description":"Invalid."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Not found (or not yours)."},"429":{"$ref":"#/components/responses/RateLimited"}},"x-required-scope":"collections:write"},"delete":{"tags":["Photos & Collections"],"summary":"Delete a photo collection","description":"Answers 409 `in_use` with `usedBy` while offerings link to it. Add `?force=1` to delete anyway; those offerings keep their photos as a plain gallery. Required scope: `collections:write` (Create, edit, duplicate and delete your photo collections.) Accounts: vendor.","security":[{"bearerAuth":["collections:write"]},{"apiKeyHeader":[]},{"sessionCookie":[]}],"responses":{"200":{"description":"Deleted.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"},"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Not found (or not yours)."},"409":{"description":"`in_use`."},"429":{"$ref":"#/components/responses/RateLimited"}},"x-required-scope":"collections:write"}},"/api/v1/vendor/collections/{id}/duplicate":{"post":{"tags":["Photos & Collections"],"summary":"Duplicate a photo collection","description":"Creates an independent copy named \"<name> (copy)\". Offerings that used the original keep using the original. Required scope: `collections:write` (Create, edit, duplicate and delete your photo collections.) Accounts: vendor.","security":[{"bearerAuth":["collections:write"]},{"apiKeyHeader":[]},{"sessionCookie":[]}],"responses":{"201":{"description":"The new collection view.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"},"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"Idempotent-Replayed":{"$ref":"#/components/headers/Idempotent-Replayed"}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Not found (or not yours)."},"429":{"$ref":"#/components/responses/RateLimited"}},"x-required-scope":"collections:write","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}]}},"/api/v1/escrow/checkout":{"post":{"tags":["Escrow & Billing"],"summary":"Create Plannyfy escrow checkout session & payout plan","description":"Computes per-category platform commission, optional 100% fee-back trip protection, and per-vendor escrow payout allocations for a winning `price` or `value` mix, records the selected or newly added customer payment method, and initializes a Plannyfy Escrow Checkout session. Required scope: `escrow:write` (Start an escrow checkout for one of your trips.) Accounts: customer.","security":[{"bearerAuth":["escrow:write"]},{"apiKeyHeader":[]},{"sessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["tripId"],"properties":{"tripId":{"type":"string","example":"trip-italy-anniversary"},"pick":{"type":"string","enum":["price","value"],"default":"price"},"withInsurance":{"type":"boolean","default":false},"paymentMethodId":{"type":"string","example":"pm-visa-4242"},"paymentMethodType":{"type":"string","enum":["card","ach_bank","express_wallet"],"default":"card"},"paymentMethodLabel":{"type":"string","example":"Visa Infinite •••• 4242"}}}}}},"responses":{"200":{"description":"Plannyfy escrow checkout session, selected payment method, and vendor payout breakdown.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"},"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"Idempotent-Replayed":{"$ref":"#/components/headers/Idempotent-Replayed"}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}},"x-required-scope":"escrow:write","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}]}},"/api/v1/billing/payment-methods":{"get":{"tags":["Escrow & Billing"],"summary":"List customer payment methods or vendor payout receiving accounts","description":"Returns saved payment methods for a customer or payout receiving accounts & commission invoice settings for a vendor (`?role=vendor`). Required scope: `billing:read` (Read your saved payment methods (never full card numbers).) Accounts: vendor, customer.","security":[{"bearerAuth":["billing:read"]},{"apiKeyHeader":[]},{"sessionCookie":[]}],"parameters":[{"name":"role","in":"query","required":false,"schema":{"type":"string","enum":["customer","vendor"],"default":"customer"}}],"responses":{"200":{"description":"Customer payment wallet or vendor payout receiving configuration.","content":{"application/json":{"schema":{"type":"object","properties":{"paymentMethods":{"type":"array","description":"Customer wallet (`role=customer`).","items":{"type":"object","properties":{"id":{"type":"string","example":"pm-visa-4242"},"type":{"type":"string","enum":["card","ach_bank","express_wallet","link"]},"brand":{"type":"string","example":"Visa Infinite"},"last4":{"type":"string","example":"4242","description":"Empty for `link`, which has no card or account number."},"email":{"type":"string","format":"email","description":"Only for `link`: the email of the Link account."},"expiry":{"type":"string","example":"08/28"},"isDefault":{"type":"boolean"}}}}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"},"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}},"x-required-scope":"billing:read"},"post":{"tags":["Escrow & Billing"],"summary":"Add a customer payment method or vendor receiving account","description":"Adds a tokenized card, ACH bank account, or express wallet to the customer wallet, or adds a receiving bank/instant-debit/SWIFT account for a vendor. Only available from the signed-in Plannyfy app: an API key gets `403` (`session_only`). Accounts: vendor, customer.","security":[{"sessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"role":{"type":"string","enum":["customer","vendor"],"default":"customer"},"type":{"type":"string","enum":["card","ach_bank","express_wallet"]},"methodType":{"type":"string","enum":["bank_transfer","instant_debit","swift_wire"]},"brand":{"type":"string","example":"Visa Infinite"},"institutionName":{"type":"string","example":"Mercury Business Checking"},"last4":{"type":"string","example":"4242"},"expiry":{"type":"string","example":"08/28"},"currency":{"type":"string","enum":["USD","EUR","GBP"]}}}}}},"responses":{"200":{"description":"Created payment method or receiving account.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"},"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"Idempotent-Replayed":{"$ref":"#/components/headers/Idempotent-Replayed"}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}},"x-session-only":true,"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}]},"patch":{"tags":["Escrow & Billing"],"summary":"Switch payment method on a customer or vendor invoice","description":"Updates the payment method attached to a customer escrow receipt (`#ESC-...`) or vendor commission tax invoice (`#INV-PFY-...`). Only available from the signed-in Plannyfy app: an API key gets `403` (`session_only`). Accounts: vendor, customer.","security":[{"sessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"role":{"type":"string","enum":["customer","vendor"],"default":"customer"},"tripId":{"type":"string","example":"trip-italy-anniversary"},"paymentMethodId":{"type":"string","example":"pm-mc-8819"},"invoiceId":{"type":"string","example":"INV-PFY-8820AB"},"mode":{"type":"string","enum":["escrow_net_deduction","corporate_card","ach_debit"]}}}}}},"responses":{"200":{"description":"Updated invoice payment record.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"},"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}},"x-session-only":true}},"/api/v1/vendor/opportunities":{"get":{"tags":["Opportunities & Bids"],"summary":"List matched opportunities","description":"Open trips and events that match your services and regions, best match first. By default opportunities you already bid on are left out. Sub-vendor accounts get `403`: their parent vendor bids. Required scope: `opportunities:read` (Read the open trips and events that match your services.) Accounts: vendor.","parameters":[{"name":"matched","in":"query","schema":{"type":"boolean","default":true},"description":"`false` lists every open opportunity, not only matches."},{"name":"includeBid","in":"query","schema":{"type":"boolean","default":false},"description":"Include opportunities you already have a live bid on."},{"name":"packageType","in":"query","schema":{"type":"string","enum":["travel","wedding","corporate_event","private_event","wellness_retreat","custom"]}},{"name":"category","in":"query","schema":{"type":"string"},"description":"Only trips with an item in this category (for example `hotel`, `catering`)."},{"name":"q","in":"query","schema":{"type":"string","maxLength":100},"description":"Title or destination contains."},{"name":"closingWithinHours","in":"query","schema":{"type":"number","exclusiveMinimum":0}},{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Cursor"}],"responses":{"200":{"description":"A page of opportunities.","content":{"application/json":{"schema":{"type":"object","required":["ok","opportunities","page"],"properties":{"ok":{"type":"boolean","enum":[true]},"version":{"type":"string","example":"v1"},"opportunities":{"type":"array","items":{"$ref":"#/components/schemas/Opportunity"}},"page":{"$ref":"#/components/schemas/Page"},"next":{"type":["string","null"],"description":"Path and query of the next page, or null on the last page."}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"},"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}},"security":[{"bearerAuth":["opportunities:read"]},{"apiKeyHeader":[]},{"sessionCookie":[]}],"x-required-scope":"opportunities:read"}},"/api/v1/vendor/opportunities/{id}":{"get":{"tags":["Opportunities & Bids"],"summary":"Get an opportunity","description":"One open opportunity with its items (including unit-pricing fields for events), sanitized `eventDetails`, `guestCount`, a competition band, your `ownBidId` when you already bid, `offerValidityHours`, and `offeringCandidates`: the offerings of yours that would fit each item, ready to link with `offeringId`. Closed, draft, own and unknown trips are `404`. Required scope: `opportunities:read` (Read the open trips and events that match your services.) Accounts: vendor.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The opportunity (trip) id."}],"responses":{"200":{"description":"The opportunity.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"version":{"type":"string"},"opportunity":{"$ref":"#/components/schemas/Opportunity"}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"},"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}},"security":[{"bearerAuth":["opportunities:read"]},{"apiKeyHeader":[]},{"sessionCookie":[]}],"x-required-scope":"opportunities:read"}},"/api/v1/vendor/bids":{"get":{"tags":["Opportunities & Bids"],"summary":"List your bids","description":"Your own bids. Live bids come most-at-risk first, past ones newest first. Bid Health is a band, never a rival's price or the traveler's budget. Required scope: `bids:read` (Read your own bids.) Accounts: vendor.","parameters":[{"name":"state","in":"query","schema":{"type":"string","enum":["live","past","all"],"default":"live"}},{"name":"tripId","in":"query","schema":{"type":"string"}},{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Cursor"}],"responses":{"200":{"description":"A page of bids.","content":{"application/json":{"schema":{"type":"object","required":["ok","bids","page"],"properties":{"ok":{"type":"boolean","enum":[true]},"version":{"type":"string","example":"v1"},"bids":{"type":"array","items":{"$ref":"#/components/schemas/Bid"}},"page":{"$ref":"#/components/schemas/Page"},"next":{"type":["string","null"],"description":"Path and query of the next page, or null on the last page."}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"},"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}},"security":[{"bearerAuth":["bids:read"]},{"apiKeyHeader":[]},{"sessionCookie":[]}],"x-required-scope":"bids:read"},"post":{"tags":["Opportunities & Bids"],"summary":"Place a bid","description":"Places a live bid with exactly the composer's rules: the trip is open, not yours, not closed and unchanged since `tripRevision`; you have no other live bid on it; every line passes the trip's price limits, unit pricing and minimum-guest rules; every `offeringId` is verified as yours. Send an `Idempotency-Key` so a retry cannot place the bid twice. A bid cannot be created as a draft: save a draft of an existing bid with PATCH `publish: false`. Required scope: `bids:write` (Place, revise and save drafts of your own bids.) Accounts: vendor.","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["tripId","lines"],"properties":{"tripId":{"type":"string"},"kind":{"type":"string","enum":["itemized","bundle"],"default":"itemized","description":"A bundle needs at least two lines."},"lines":{"type":"array","minItems":1,"maxItems":50,"items":{"$ref":"#/components/schemas/BidLineInput"}},"floorPrice":{"type":"number","description":"Your private minimum for the whole bid. Not above the bid total."},"tierDiscountPct":{"type":"integer","minimum":0,"maximum":15,"description":"Repeat-traveler discount. Defaults to your profile setting."},"tripRevision":{"type":"integer","description":"The opportunity's `revision` you priced against. A mismatch is `409 trip_changed`."}}}}}},"responses":{"201":{"description":"The new bid.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"version":{"type":"string"},"bid":{"$ref":"#/components/schemas/Bid"}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"},"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"Idempotent-Replayed":{"$ref":"#/components/headers/Idempotent-Replayed"}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"413":{"$ref":"#/components/responses/TooLarge"},"422":{"$ref":"#/components/responses/Unprocessable"},"429":{"$ref":"#/components/responses/RateLimited"}},"security":[{"bearerAuth":["bids:write"]},{"apiKeyHeader":[]},{"sessionCookie":[]}],"x-required-scope":"bids:write"}},"/api/v1/vendor/bids/{id}":{"get":{"tags":["Opportunities & Bids"],"summary":"Get a bid","description":"One of your bids, live or past. Someone else's bid is the same `404` as a missing one. Required scope: `bids:read` (Read your own bids.) Accounts: vendor.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The bid id."}],"responses":{"200":{"description":"The bid.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"version":{"type":"string"},"bid":{"$ref":"#/components/schemas/Bid"}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"},"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}},"security":[{"bearerAuth":["bids:read"]},{"apiKeyHeader":[]},{"sessionCookie":[]}],"x-required-scope":"bids:read"},"patch":{"tags":["Opportunities & Bids"],"summary":"Revise a bid or save a draft","description":"`publish: true` (the default) revises the live bid with the composer's rules, including the daily price-change cap and the below-market and below-floor confirmation: such a change answers `409 confirmation_required` with `needsConfirm: true`, and goes through when you repeat it with `confirm: true`. `publish: false` saves the lines as your private draft; nothing goes live and nothing is limited except the save rate (`lines: null` discards the draft). Lines you leave out of `lines` are removed from the bid. Required scope: `bids:write` (Place, revise and save drafts of your own bids.) Accounts: vendor.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["lines"],"properties":{"lines":{"type":["array","null"],"items":{"$ref":"#/components/schemas/BidLineInput"}},"publish":{"type":"boolean","default":true},"confirm":{"type":"boolean","default":false},"tripRevision":{"type":"integer"}}}}}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The bid id."}],"responses":{"200":{"description":"The revised bid (`published: true`) or the bid with its saved draft (`published: false`, `draftSavedAt`).","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"published":{"type":"boolean"},"stillPaused":{"type":"boolean"},"draftSavedAt":{"type":["string","null"]},"bid":{"$ref":"#/components/schemas/Bid"}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"},"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"413":{"$ref":"#/components/responses/TooLarge"},"422":{"$ref":"#/components/responses/Unprocessable"},"429":{"$ref":"#/components/responses/RateLimited"}},"security":[{"bearerAuth":["bids:write"]},{"apiKeyHeader":[]},{"sessionCookie":[]}],"x-required-scope":"bids:write"}},"/api/v1/vendor/earnings":{"get":{"tags":["Earnings & Payouts"],"summary":"List won bookings and earnings","description":"Your won bookings (newest first) with the awarded lines, commission, net and payout state, plus totals in `summary` and your `pipeline` (open bids currently winning). Only your own awarded lines are counted; refunds and dispute outcomes lower the figures once the payout ledger records them. Event trips include `guestCount` and sanitized `eventDetails`. Required scope: `earnings:read` (Read your won bookings, commission and net earnings.) Accounts: vendor.","parameters":[{"name":"payout","in":"query","schema":{"type":"string","enum":["pending","paid"]}},{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Cursor"}],"responses":{"200":{"description":"A page of bookings with `summary` and `pipeline`.","content":{"application/json":{"schema":{"type":"object","required":["ok","bookings","page"],"properties":{"ok":{"type":"boolean","enum":[true]},"version":{"type":"string","example":"v1"},"bookings":{"type":"array","items":{"type":"object"}},"page":{"$ref":"#/components/schemas/Page"},"next":{"type":["string","null"],"description":"Path and query of the next page, or null on the last page."},"currency":{"type":"string","example":"usd"},"summary":{"type":"object"},"pipeline":{"type":"array","items":{"type":"object"}}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"},"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}},"security":[{"bearerAuth":["earnings:read"]},{"apiKeyHeader":[]},{"sessionCookie":[]}],"x-required-scope":"earnings:read"}},"/api/v1/vendor/payouts":{"get":{"tags":["Earnings & Payouts"],"summary":"List payouts","description":"The state of your payouts, newest first. Amounts are USD. Admin notes, hold reasons, raw payment-provider errors and account identifiers are never included; `note` is the one-sentence explanation the app shows. Required scope: `payouts:read` (Read the state of your payouts.) Accounts: vendor.","parameters":[{"name":"status","in":"query","schema":{"type":"string","enum":["pending","eligible","awaiting_approval","processing","paid","failed","held","reversed","cancelled"]}},{"name":"tripId","in":"query","schema":{"type":"string"}},{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Cursor"}],"responses":{"200":{"description":"A page of payouts.","content":{"application/json":{"schema":{"type":"object","required":["ok","payouts","page"],"properties":{"ok":{"type":"boolean","enum":[true]},"version":{"type":"string","example":"v1"},"payouts":{"type":"array","items":{"$ref":"#/components/schemas/Payout"}},"page":{"$ref":"#/components/schemas/Page"},"next":{"type":["string","null"],"description":"Path and query of the next page, or null on the last page."}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"},"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}},"security":[{"bearerAuth":["payouts:read"]},{"apiKeyHeader":[]},{"sessionCookie":[]}],"x-required-scope":"payouts:read"}},"/api/v1/vendor/webhooks":{"get":{"tags":["Webhooks & Events"],"summary":"Read webhook settings","description":"Your endpoint URL, the events you subscribe to, the last 20 deliveries (id, event, mode, statusCode, statusText, deliveredAt) and the events that exist. The signing secret is never returned. Required scope: `webhooks:read` (Read your webhook endpoint settings and recent deliveries (the secret is never returned).) Accounts: vendor.","responses":{"200":{"description":"Settings and recent deliveries.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"webhook":{"$ref":"#/components/schemas/WebhookSettings"},"deliveries":{"type":"array","items":{"type":"object"}},"availableEvents":{"type":"array","items":{"type":"object"}}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"},"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}},"security":[{"bearerAuth":["webhooks:read"]},{"apiKeyHeader":[]},{"sessionCookie":[]}],"x-required-scope":"webhooks:read"},"put":{"tags":["Webhooks & Events"],"summary":"Change webhook settings","description":"Sets the endpoint URL and/or the subscribed events; fields you leave out stay as they are and `url: null` removes the endpoint. The URL must be `https` and a public host (no credentials, no localhost, private or link-local address). The first time an endpoint is set, a signing secret is created: fetch it with `rotate-secret`. Required scope: `webhooks:write` (Change your webhook endpoint and events, and rotate the webhook signing secret.) Accounts: vendor.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":["string","null"],"format":"uri"},"events":{"type":"array","items":{"type":"string","enum":["opportunity.matched","bid.status_changed","trip.locked","dispute.updated"]}}}}}}},"responses":{"200":{"description":"The new settings.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"webhook":{"$ref":"#/components/schemas/WebhookSettings"}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"},"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"413":{"$ref":"#/components/responses/TooLarge"},"422":{"$ref":"#/components/responses/Unprocessable"},"429":{"$ref":"#/components/responses/RateLimited"}},"security":[{"bearerAuth":["webhooks:write"]},{"apiKeyHeader":[]},{"sessionCookie":[]}],"x-required-scope":"webhooks:write"}},"/api/v1/vendor/webhooks/rotate-secret":{"post":{"tags":["Webhooks & Events"],"summary":"Rotate the webhook signing secret","description":"Replaces your signing secret and returns the new one, this once. The old secret stops signing at once, so update your receiver first, or accept both for a while. Not idempotent: `Idempotency-Key` is ignored here so the secret is never kept in a stored answer. Required scope: `webhooks:write` (Change your webhook endpoint and events, and rotate the webhook signing secret.) Accounts: vendor.","responses":{"200":{"description":"The new secret.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"webhookSecret":{"type":"string","example":"whsec_plannyfy_…"},"rotatedAt":{"type":"string","format":"date-time"},"note":{"type":"string"}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"},"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"Idempotent-Replayed":{"$ref":"#/components/headers/Idempotent-Replayed"}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}},"security":[{"bearerAuth":["webhooks:write"]},{"apiKeyHeader":[]},{"sessionCookie":[]}],"x-required-scope":"webhooks:write"}},"/api/v1/vendor/bids/{id}/withdraw":{"post":{"tags":["Opportunities & Bids"],"summary":"Withdraw a live bid","description":"Withdraws one of your live bids before lock-in with a reason (`trip_changed`, `force_majeure`, `supplier_cancelled`, `pricing_mistake` or `other`). `trip_changed` is only accepted when the traveler edited the trip after you priced the bid. Required scope: `bids:write` (Place, revise and save drafts of your own bids.) Accounts: vendor.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The bid id."},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["reason"],"properties":{"reason":{"type":"string","enum":["trip_changed","force_majeure","supplier_cancelled","pricing_mistake","other"]},"note":{"type":"string","maxLength":300},"evidenceNote":{"type":"string","maxLength":300}}}}}},"responses":{"200":{"description":"The withdrawn bid status.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"version":{"type":"string"},"id":{"type":"string"},"status":{"type":"string","enum":["withdrawn"]},"withdrawal":{"type":["object","null"]}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"},"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"Idempotent-Replayed":{"$ref":"#/components/headers/Idempotent-Replayed"}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"422":{"$ref":"#/components/responses/Unprocessable"},"429":{"$ref":"#/components/responses/RateLimited"}},"security":[{"bearerAuth":["bids:write"]},{"apiKeyHeader":[]},{"sessionCookie":[]}],"x-required-scope":"bids:write"}},"/api/v1/vendor/bids/{id}/confirm":{"post":{"tags":["Opportunities & Bids"],"summary":"Confirm price after a trip change","description":"Confirms that your live bid's current price still works after the traveler changed items on the trip, clearing `needsReprice` without counting against your daily price-change cap. Required scope: `bids:write` (Place, revise and save drafts of your own bids.) Accounts: vendor.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The bid id."},{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"200":{"description":"The unpaused bid.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"version":{"type":"string"},"id":{"type":"string"},"confirmedAt":{"type":"string","format":"date-time"},"bid":{"$ref":"#/components/schemas/Bid"}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"},"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"Idempotent-Replayed":{"$ref":"#/components/headers/Idempotent-Replayed"}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/RateLimited"}},"security":[{"bearerAuth":["bids:write"]},{"apiKeyHeader":[]},{"sessionCookie":[]}],"x-required-scope":"bids:write"}},"/api/v1/vendor/availability":{"get":{"tags":["Opportunities & Bids"],"summary":"Read availability and blackout dates","description":"Returns your availability calendar settings: `maxEventsPerDay`, `leadTimeDays`, and `blockedRanges` (vendor-wide or offering-scoped blackout date ranges). Required scope: `availability:read` (Read your availability calendar, blackout windows and daily capacity limits.) Accounts: vendor.","responses":{"200":{"description":"Your availability configuration.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"version":{"type":"string"},"vendorId":{"type":"string"},"availability":{"type":"object","properties":{"maxEventsPerDay":{"type":["integer","null"]},"leadTimeDays":{"type":["integer","null"]},"blockedRanges":{"type":"array","items":{"type":"object"}},"updatedAt":{"type":["string","null"]}}}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"},"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}},"security":[{"bearerAuth":["availability:read"]},{"apiKeyHeader":[]},{"sessionCookie":[]}],"x-required-scope":"availability:read"},"put":{"tags":["Opportunities & Bids"],"summary":"Update availability and blackout dates","description":"Replaces or updates your availability settings (`maxEventsPerDay` 1–20, `leadTimeDays` 0–365, and up to 100 `blockedRanges` with `startDate` and `endDate` in `YYYY-MM-DD`). Required scope: `availability:write` (Update your blackout date ranges, minimum notice and daily capacity limits.) Accounts: vendor.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"maxEventsPerDay":{"type":["integer","null"],"minimum":1,"maximum":20},"leadTimeDays":{"type":["integer","null"],"minimum":0,"maximum":365},"blockedRanges":{"type":"array","maxItems":100,"items":{"type":"object","required":["startDate"],"properties":{"id":{"type":"string"},"startDate":{"type":"string","format":"date"},"endDate":{"type":"string","format":"date"},"reason":{"type":"string","maxLength":160},"offeringIds":{"type":"array","items":{"type":"string"}}}}}}}}}},"responses":{"200":{"description":"Updated availability configuration.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"version":{"type":"string"},"vendorId":{"type":"string"},"availability":{"type":"object"}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"},"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"422":{"$ref":"#/components/responses/Unprocessable"},"429":{"$ref":"#/components/responses/RateLimited"}},"security":[{"bearerAuth":["availability:write"]},{"apiKeyHeader":[]},{"sessionCookie":[]}],"x-required-scope":"availability:write"}},"/api/v1/trips":{"get":{"tags":["Customer Trips & Concierge"],"summary":"List your trips","description":"Lists the authenticated customer's own trips and event packages, newest first, with optional `status` and `packageType` filters. Required scope: `trips:read` (Read your own trips, items, live optimal mix picks and escrow state.) Accounts: customer.","parameters":[{"name":"status","in":"query","schema":{"type":"string","enum":["draft","open","locked","cancelled"]}},{"name":"packageType","in":"query","schema":{"type":"string","enum":["travel","wedding","corporate_event","private_event","wellness_retreat","custom"]}},{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Cursor"}],"responses":{"200":{"description":"A page of your trips.","content":{"application/json":{"schema":{"type":"object","required":["ok","trips","page"],"properties":{"ok":{"type":"boolean","enum":[true]},"version":{"type":"string","example":"v1"},"trips":{"type":"array","items":{"type":"object"}},"page":{"$ref":"#/components/schemas/Page"},"next":{"type":["string","null"],"description":"Path and query of the next page, or null on the last page."}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"},"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}},"security":[{"bearerAuth":["trips:read"]},{"apiKeyHeader":[]},{"sessionCookie":[]}],"x-required-scope":"trips:read"},"post":{"tags":["Customer Trips & Concierge"],"summary":"Create a trip or event package","description":"Creates a new trip or event package for the authenticated customer. When `draft: false` (default), opens the package to matched vendors immediately. If `items` is omitted, generates a default hotel + experience item set. Required scope: `trips:write` (Create, publish and edit your own trips or event packages.) Accounts: customer.","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["destination","startDate","endDate","targetBudget"],"properties":{"title":{"type":"string","maxLength":140},"destination":{"type":"string","maxLength":140},"startDate":{"type":"string","format":"date"},"endDate":{"type":"string","format":"date"},"travelers":{"type":"integer","minimum":1,"maximum":300,"default":2},"guestCount":{"type":"integer","minimum":1},"targetBudget":{"type":"number","minimum":500},"maxBudget":{"type":"number"},"packageType":{"type":"string","enum":["travel","wedding","corporate_event","private_event","wellness_retreat","custom"],"default":"travel"},"priority":{"type":"string","enum":["price","value","balanced"],"default":"balanced"},"draft":{"type":"boolean","default":false},"items":{"type":"array","maxItems":30,"items":{"type":"object","required":["title"],"properties":{"id":{"type":"string"},"category":{"type":"string"},"title":{"type":"string"},"details":{"type":"string"},"location":{"type":"string"},"startDate":{"type":"string","format":"date"},"endDate":{"type":"string","format":"date"},"quantity":{"type":"integer","minimum":1},"referencePrice":{"type":"number"},"targetPrice":{"type":"number"},"required":{"type":"boolean"}}}}}}}}},"responses":{"201":{"description":"The created trip.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"version":{"type":"string"},"trip":{"type":"object"}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"},"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"Idempotent-Replayed":{"$ref":"#/components/headers/Idempotent-Replayed"}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"422":{"$ref":"#/components/responses/Unprocessable"},"429":{"$ref":"#/components/responses/RateLimited"}},"security":[{"bearerAuth":["trips:write"]},{"apiKeyHeader":[]},{"sessionCookie":[]}],"x-required-scope":"trips:write"}},"/api/v1/trips/{id}":{"get":{"tags":["Customer Trips & Concierge"],"summary":"Get a trip and its live optimal mix","description":"Returns one of your own trips with its items, the live `#1 Best Price` (`picks.price`) and `#1 Best Value` (`picks.value`) optimal mix solutions, item-level offers (`rows`), refunds and disputes. Required scope: `trips:read` (Read your own trips, items, live optimal mix picks and escrow state.) Accounts: customer.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The trip id."}],"responses":{"200":{"description":"The trip, live optimal mix picks, offers, refunds and disputes.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"version":{"type":"string"},"trip":{"type":"object"},"picks":{"type":"object"},"rows":{"type":"array","items":{"type":"object"}},"liveBidsCount":{"type":"integer"},"refunds":{"type":"array","items":{"type":"object"}},"disputes":{"type":"array","items":{"type":"object"}}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"},"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}},"security":[{"bearerAuth":["trips:read"]},{"apiKeyHeader":[]},{"sessionCookie":[]}],"x-required-scope":"trips:read"},"patch":{"tags":["Customer Trips & Concierge"],"summary":"Edit a trip or change priority","description":"Publishes a draft (`publish: true`), updates `priority` (`price`, `value`, `balanced`), or edits trip details and items (`title`, `startDate`, `endDate`, `travelers`, `targetBudget`, `maxBudget`, `items`, `addItems`, `removeItemIds`). Material edits flag affected live vendor bids for re-pricing. Required scope: `trips:write` (Create, publish and edit your own trips or event packages.) Accounts: customer.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The trip id."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"publish":{"type":"boolean"},"priority":{"type":"string","enum":["price","value","balanced"]},"title":{"type":"string"},"startDate":{"type":"string","format":"date"},"endDate":{"type":"string","format":"date"},"travelers":{"type":"integer"},"targetBudget":{"type":"number"},"maxBudget":{"type":["number","null"]},"applyNow":{"type":"boolean","default":true},"items":{"type":"array","items":{"type":"object"}},"addItems":{"type":"array","items":{"type":"object"}},"removeItemIds":{"type":"array","items":{"type":"string"}}}}}}},"responses":{"200":{"description":"The updated trip and edit impact.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"version":{"type":"string"},"status":{"type":"string"},"trip":{"type":"object"}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"},"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"422":{"$ref":"#/components/responses/Unprocessable"},"429":{"$ref":"#/components/responses/RateLimited"}},"security":[{"bearerAuth":["trips:write"]},{"apiKeyHeader":[]},{"sessionCookie":[]}],"x-required-scope":"trips:write"}},"/api/v1/trips/{id}/lock":{"post":{"tags":["Customer Trips & Concierge"],"summary":"Lock a winning package mix","description":"Locks in the winning bid mix (`pick: \"price\" | \"value\"`) on one of your open trips at `confirmedTotal`, recording the winning vendor allocations and notifying winning vendors. Required scope: `escrow:write` (Start an escrow checkout for one of your trips.) Accounts: customer.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The trip id."},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"pick":{"type":"string","enum":["price","value"],"default":"price"},"confirmedTotal":{"type":"number","description":"Expected total in USD. Defaults to the current optimal mix total."},"withInsurance":{"type":"boolean","default":false},"paymentMethodId":{"type":"string"},"paymentIntentId":{"type":"string"},"splitSharesCount":{"type":"integer","minimum":1,"maximum":12},"paymentPlan":{"type":"string","enum":["full","deposit_balance"],"default":"full"},"depositPercent":{"type":"integer"},"balanceDueDaysBeforeStart":{"type":"integer"}}}}}},"responses":{"200":{"description":"The locked trip and allocation snapshot.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"version":{"type":"string"},"total":{"type":"number"},"trip":{"type":"object"}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"},"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"Idempotent-Replayed":{"$ref":"#/components/headers/Idempotent-Replayed"}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/RateLimited"}},"security":[{"bearerAuth":["escrow:write"]},{"apiKeyHeader":[]},{"sessionCookie":[]}],"x-required-scope":"escrow:write"}},"/api/v1/trips/{id}/complete":{"post":{"tags":["Customer Trips & Concierge"],"summary":"Sign off post-trip completion or report an issue","description":"Records your post-trip 48-hour completion sign-off (`status: \"confirmed\"`, clearing escrow for payout release) or reports a post-trip issue (`status: \"issue_reported\"` with `issueSummary`, placing unreleased vendor payouts on hold). Required scope: `escrow:write` (Start an escrow checkout for one of your trips.) Accounts: customer.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The trip id."},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["confirmed","issue_reported"],"default":"confirmed"},"issueSummary":{"type":"string","maxLength":500}}}}}},"responses":{"200":{"description":"The recorded completion sign-off.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"version":{"type":"string"},"tripId":{"type":"string"},"signOff":{"type":"object"}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"},"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"Idempotent-Replayed":{"$ref":"#/components/headers/Idempotent-Replayed"}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/RateLimited"}},"security":[{"bearerAuth":["escrow:write"]},{"apiKeyHeader":[]},{"sessionCookie":[]}],"x-required-scope":"escrow:write"}},"/api/v1/trips/{id}/cancel":{"post":{"tags":["Customer Trips & Concierge"],"summary":"Cancel an open or locked trip","description":"Cancels an open trip, deletes an unpublished draft, or cancels a locked trip according to its cancellation policy and issues any due traveler refund. Required scope: `escrow:write` (Start an escrow checkout for one of your trips.) Accounts: customer.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The trip id."},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"reason":{"type":"string","maxLength":240},"expectedRefundCents":{"type":"integer"}}}}}},"responses":{"200":{"description":"The cancellation result and any issued refund.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"version":{"type":"string"},"tripId":{"type":"string"},"stage":{"type":"string"},"settlement":{"type":"object"},"refund":{"type":"object"},"trip":{"type":"object"}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"},"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"Idempotent-Replayed":{"$ref":"#/components/headers/Idempotent-Replayed"}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/RateLimited"}},"security":[{"bearerAuth":["escrow:write"]},{"apiKeyHeader":[]},{"sessionCookie":[]}],"x-required-scope":"escrow:write"}},"/api/v1/trips/{id}/disputes":{"post":{"tags":["Customer Trips & Concierge"],"summary":"Open a dispute on a locked trip","description":"Opens a formal escrow dispute on one of your locked trips (`service_mismatch`, `billing_dispute`, `post_lock_cancellation`, or `force_majeure_claim`). Required scope: `escrow:write` (Start an escrow checkout for one of your trips.) Accounts: customer.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The trip id."},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["summary"],"properties":{"reason":{"type":"string","enum":["service_mismatch","billing_dispute","post_lock_cancellation","force_majeure_claim"],"default":"service_mismatch"},"summary":{"type":"string","minLength":5,"maxLength":400},"amountInDispute":{"type":"number"},"vendorId":{"type":"string"},"vendorName":{"type":"string"},"bidId":{"type":"string"}}}}}},"responses":{"201":{"description":"The created dispute.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"version":{"type":"string"},"dispute":{"type":"object"}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"},"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"Idempotent-Replayed":{"$ref":"#/components/headers/Idempotent-Replayed"}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/RateLimited"}},"security":[{"bearerAuth":["escrow:write"]},{"apiKeyHeader":[]},{"sessionCookie":[]}],"x-required-scope":"escrow:write"}},"/api/v1/trips/{id}/messages":{"get":{"tags":["Customer Trips & Concierge"],"summary":"List moderated trip messages","description":"Lists moderated concierge and Q&A messages on a trip. Customers see all threads on their trip (or filter with `?vendorId=`); vendors see only their own thread. Required scope: `messages:read` (Read moderated concierge and Q&A messages on trips you own or bid on.) Accounts: vendor, customer.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The trip id."},{"name":"vendorId","in":"query","schema":{"type":"string"},"description":"Customer callers: filter to one vendor thread."}],"responses":{"200":{"description":"Messages on the trip.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"version":{"type":"string"},"tripId":{"type":"string"},"messages":{"type":"array","items":{"type":"object"}},"count":{"type":"integer"}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"},"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}},"security":[{"bearerAuth":["messages:read"]},{"apiKeyHeader":[]},{"sessionCookie":[]}],"x-required-scope":"messages:read"},"post":{"tags":["Customer Trips & Concierge"],"summary":"Send a moderated trip message","description":"Sends a message on a trip thread. Before the booking is locked, emails, phone numbers, URLs and messaging handles are automatically replaced with `[contact hidden until booking is locked]`; once locked, contact info passes through. Required scope: `messages:write` (Send moderated concierge and Q&A messages on trips you own or bid on.) Accounts: vendor, customer.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The trip id."},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["body"],"properties":{"body":{"type":"string","minLength":1,"maxLength":1500},"vendorId":{"type":"string","description":"Required when calling as a customer."},"vendorName":{"type":"string"}}}}}},"responses":{"201":{"description":"The sent (and moderated) message.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"version":{"type":"string"},"message":{"type":"object"},"bookingLocked":{"type":"boolean"}}}}},"headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"},"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"Idempotent-Replayed":{"$ref":"#/components/headers/Idempotent-Replayed"}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/RateLimited"}},"security":[{"bearerAuth":["messages:write"]},{"apiKeyHeader":[]},{"sessionCookie":[]}],"x-required-scope":"messages:write"}}},"webhooks":{"opportunity.matched":{"post":{"tags":["Webhooks & Events"],"summary":"A trip you can bid on opened","description":"Sent when a traveler publishes a trip that matches your portfolio. Never contains the traveler's budget, per-item targets or any rival's price. Fetch the full opportunity with GET /api/v1/vendor/opportunities/{tripId}.\n\nAs HTTP: a `POST` with `Content-Type: application/json`, `X-Plannyfy-Event: opportunity.matched`, `User-Agent: Plannyfy-Webhooks/1.0` and `X-Plannyfy-Signature: t=<unix_seconds>,v1=<hex>` (HMAC-SHA256, key = your webhook secret, message = `<t>.<raw request body>`). Verify it over the raw bytes before you parse the JSON, compare in constant time and reject timestamps more than 5 minutes old.\n\n**Delivery:** one attempt with a 3.5 second timeout; a 2xx response means received. There is no automatic retry yet, so treat webhooks as a hint and reconcile with `GET /api/v1/vendor/opportunities`, `/bids` and `/payouts`. The last 20 deliveries, with the status each endpoint answered, are in `GET /api/v1/vendor/webhooks`. De-duplicate on the envelope `id`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/WebhookEnvelope"},{"type":"object","properties":{"event":{"enum":["opportunity.matched"]},"data":{"type":"object","properties":{"tripId":{"type":"string"},"tripTitle":{"type":"string"},"destination":{"type":"string"},"startDate":{"type":"string"},"endDate":{"type":"string"},"travelers":{"type":"integer"},"priority":{"type":"string"},"biddingClosesAt":{"type":"string","format":"date-time"},"marketBenchmarkTotal":{"type":"number"},"matchedPortfolioItems":{"type":"array","items":{"type":"object","properties":{"tripItemId":{"type":"string"},"tripItemTitle":{"type":"string"},"category":{"type":"string"},"portfolioItemId":{"type":"string","description":"Use as `offeringId` on a bid line."},"portfolioExternalId":{"type":"string"},"portfolioTitle":{"type":"string"},"suggestedQuoteUsd":{"type":"number"},"includedPerks":{"type":"array","items":{"type":"string"}}}}},"composeBidUrl":{"type":"string","format":"uri"}}}}}]}}}},"responses":{"200":{"description":"Return any 2xx to acknowledge."}}}},"bid.status_changed":{"post":{"tags":["Webhooks & Events"],"summary":"Your live bid needs attention","description":"Sent when a traveler's edit pauses your bid for re-pricing, or when other activity moves your bid into a close call or out of the winning zone. Never names or prices a rival.\n\nAs HTTP: a `POST` with `Content-Type: application/json`, `X-Plannyfy-Event: bid.status_changed`, `User-Agent: Plannyfy-Webhooks/1.0` and `X-Plannyfy-Signature: t=<unix_seconds>,v1=<hex>` (HMAC-SHA256, key = your webhook secret, message = `<t>.<raw request body>`). Verify it over the raw bytes before you parse the JSON, compare in constant time and reject timestamps more than 5 minutes old.\n\n**Delivery:** one attempt with a 3.5 second timeout; a 2xx response means received. There is no automatic retry yet, so treat webhooks as a hint and reconcile with `GET /api/v1/vendor/opportunities`, `/bids` and `/payouts`. The last 20 deliveries, with the status each endpoint answered, are in `GET /api/v1/vendor/webhooks`. De-duplicate on the envelope `id`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/WebhookEnvelope"},{"type":"object","properties":{"event":{"enum":["bid.status_changed"]},"data":{"type":"object","properties":{"bidId":{"type":"string"},"tripId":{"type":"string"},"tripTitle":{"type":"string"},"previousStatus":{"type":"string"},"newStatus":{"type":"string"},"reason":{"type":"string"},"yourCurrentTotalUsd":{"type":"number"},"actionRequired":{"type":"string"},"respondBy":{"type":"string","format":"date-time"},"bidStudioUrl":{"type":"string","format":"uri"}}}}}]}}}},"responses":{"200":{"description":"Return any 2xx to acknowledge."}}}},"trip.locked":{"post":{"tags":["Webhooks & Events"],"summary":"You won lines on a trip","description":"Sent to each winning vendor when a traveler locks in, with your awarded lines, commission and net escrow payout.\n\nAs HTTP: a `POST` with `Content-Type: application/json`, `X-Plannyfy-Event: trip.locked`, `User-Agent: Plannyfy-Webhooks/1.0` and `X-Plannyfy-Signature: t=<unix_seconds>,v1=<hex>` (HMAC-SHA256, key = your webhook secret, message = `<t>.<raw request body>`). Verify it over the raw bytes before you parse the JSON, compare in constant time and reject timestamps more than 5 minutes old.\n\n**Delivery:** one attempt with a 3.5 second timeout; a 2xx response means received. There is no automatic retry yet, so treat webhooks as a hint and reconcile with `GET /api/v1/vendor/opportunities`, `/bids` and `/payouts`. The last 20 deliveries, with the status each endpoint answered, are in `GET /api/v1/vendor/webhooks`. De-duplicate on the envelope `id`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/WebhookEnvelope"},{"type":"object","properties":{"event":{"enum":["trip.locked"]},"data":{"type":"object","properties":{"tripId":{"type":"string"},"tripTitle":{"type":"string"},"destination":{"type":"string"},"startDate":{"type":"string"},"endDate":{"type":"string"},"travelers":{"type":"integer"},"bidId":{"type":"string"},"selectedPick":{"type":"string","enum":["price","value"]},"allocations":{"type":"array","items":{"type":"object","properties":{"itemId":{"type":"string"},"title":{"type":"string"},"category":{"type":"string"},"grossPriceUsd":{"type":"number"},"platformFeePct":{"type":"number"},"platformFeeUsd":{"type":"number"},"netEscrowPayoutUsd":{"type":"number"}}}},"grossTotalUsd":{"type":"number"},"platformFeeUsd":{"type":"number"},"netEscrowPayoutUsd":{"type":"number"},"escrowStatus":{"type":"string","example":"funded_held"},"earningsUrl":{"type":"string","format":"uri"}}}}}]}}}},"responses":{"200":{"description":"Return any 2xx to acknowledge."}}}},"dispute.updated":{"post":{"tags":["Webhooks & Events"],"summary":"A dispute or withdrawal review changed","description":"Sent when a post-lock issue is opened or resolved, or a bid withdrawal review is decided.\n\nAs HTTP: a `POST` with `Content-Type: application/json`, `X-Plannyfy-Event: dispute.updated`, `User-Agent: Plannyfy-Webhooks/1.0` and `X-Plannyfy-Signature: t=<unix_seconds>,v1=<hex>` (HMAC-SHA256, key = your webhook secret, message = `<t>.<raw request body>`). Verify it over the raw bytes before you parse the JSON, compare in constant time and reject timestamps more than 5 minutes old.\n\n**Delivery:** one attempt with a 3.5 second timeout; a 2xx response means received. There is no automatic retry yet, so treat webhooks as a hint and reconcile with `GET /api/v1/vendor/opportunities`, `/bids` and `/payouts`. The last 20 deliveries, with the status each endpoint answered, are in `GET /api/v1/vendor/webhooks`. De-duplicate on the envelope `id`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/WebhookEnvelope"},{"type":"object","properties":{"event":{"enum":["dispute.updated"]},"data":{"type":"object","properties":{"disputeId":{"type":"string"},"tripId":{"type":"string"},"tripTitle":{"type":"string"},"bidId":{"type":"string"},"stage":{"type":"string","enum":["opened","resolved","withdrawal_reviewed"]},"status":{"type":"string"},"reason":{"type":"string"},"amountInDisputeUsd":{"type":"number"},"summary":{"type":"string"},"resolution":{"type":"string"},"resolutionNote":{"type":"string"},"resolvedAt":{"type":"string","format":"date-time"}}}}}]}}}},"responses":{"200":{"description":"Return any 2xx to acknowledge."}}}}}}