Skip to main content
Plannyfy API v1.7.0 · OpenAPI 3.1.0

Plannyfy API & Signed Webhooks

Connect your hotel PMS, villa catalog, yacht fleet, sub-vendor network, or DMC rate sheets directly to Plannyfy. Push rich offerings with 4K video and seasonal tiers via REST, let Plannyfy poll your read-only feed, and subscribe to real-time HMAC-SHA256 webhooks.

API key authentication
Authorization: Bearer pfy_live_••••••••••••
# or header: X-Plannyfy-Api-Key: pfy_live_••••
POST /api/v1/vendor/portfolio
curl -X POST "https://plannyfy.com/api/v1/vendor/portfolio" \
  -H "Authorization: Bearer pfy_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "version": "v1",
    "items": [
      {
        "externalId": "pal-roma-01",
        "category": "hotel",
        "title": "Hotel Palazzo Roma · 5★ Boutique Stay",
        "location": "Rome, Italy",
        "pricingUnit": "per_night",
        "basePriceUsd": 420,
        "retailReferenceUsd": 520,
        "seasonalRates": {
          "peakSummerUsd": 510,
          "shoulderUsd": 420,
          "lowSeasonUsd": 350
        },
        "floorPctOfBase": 84,
        "galleryUrls": [
          "https://images.unsplash.com/photo-1552832230-c0197dd311b5?w=1200",
          "https://images.unsplash.com/photo-1533105079780-92b9be482077?w=1200"
        ],
        "youtubeUrl": "https://www.youtube.com/watch?v=LXb3EKWsInQ",
        "videoTitle": "4K Courtyard & Rooftop Suite Walkthrough",
        "includedPerks": ["Daily breakfast included", "Welcome Prosecco", "14-day free cancellation"],
        "tripAdvisorUrl": "https://www.tripadvisor.com/Hotel_Palazzo_Roma"
      }
    ]
  }'
Upserts up to 100 offerings per call · Idempotent on `externalId`200 OK · Plannyfy Schema v1
1. Catalog, Photos & Sub-Vendors

Sync offerings via POST /api/v1/vendor/portfolio, manage sub-vendor networks via /sub-vendors, or upload WebP media & collections via /photos and /collections.

2. Opportunities, Bids & Payouts

Query matched trips with GET /api/v1/vendor/opportunities, place or revise bids with /bids (retry-safe via Idempotency-Key), and reconcile ledger state with /earnings & /payouts.

3. Outbound HMAC Webhooks

Configure your endpoint via PUT /api/v1/vendor/webhooks, rotate secrets with /rotate-secret, and receive signed X-Plannyfy-Signature events in real time.

Interactive Schema Normalizer

Send Canonical Schema v1—or your existing PMS JSON

Switch between our canonical plannyfy.travel.portfolio.v1 payload and a typical external Hotel/Tour PMS payload to see how Plannyfy normalizes both into a rich offering card.

payload.plannyfy-v1.json
Canonical v1
{
  "externalId": "belvedere-positano-01",
  "category": "hotel",
  "title": "Villa Belvedere Positano · Sea-View Terrace Suite",
  "location": "Amalfi Coast, Italy",
  "country": "Italy",
  "pricingUnit": "per_night",
  "basePriceUsd": 680,
  "retailReferenceUsd": 840,
  "seasonalRates": {
    "peakSummerUsd": 890,
    "shoulderUsd": 680,
    "lowSeasonUsd": 510
  },
  "floorPctOfBase": 85,
  "galleryUrls": [
    "https://images.unsplash.com/photo-1533105079780-92b9be482077?w=1200",
    "https://images.unsplash.com/photo-1552832230-c0197dd311b5?w=1200",
    "https://images.unsplash.com/photo-1570077188670-e3a8d69ac5ff?w=1200"
  ],
  "youtubeUrl": "https://www.youtube.com/watch?v=LXb3EKWsInQ",
  "videoTitle": "4K Cliffside Infinity Pool & Suite Walkthrough",
  "socialReelUrl": "https://www.instagram.com/reel/C8positano/",
  "highlights": [
    "Heated Cliffside Infinity Pool",
    "Private Boat Jetty Access"
  ],
  "includedPerks": [
    "Daily gourmet breakfast",
    "Sea-view terrace upgrade",
    "Welcome sunset Prosecco"
  ],
  "tripAdvisorUrl": "https://www.tripadvisor.com/Villa_Belvedere",
  "googleMapsUrl": "https://maps.google.com/?q=Positano+Italy",
  "cancellationPolicy": "moderate_7d",
  "autoMatchEnabled": true
}
Normalized Plannyfy Offering Preview✓ Validated & Indexed
Villa Belvedere Positano
HOTEL · SKU: belvedere-positano-013 Photos + 4K Video
Amalfi Coast, Italy

Villa Belvedere Positano · Sea-View Terrace Suite

Peak (Jun–Aug)
$890/n
Shoulder (Base)
$680/n
Low (Nov–Mar)
$510/n
✓ Daily gourmet breakfast✓ Sea-view terrace upgrade✓ Welcome sunset Prosecco
YouTube 4K AttachedTripAdvisor VerifiedGoogle Maps Pin
Zero custom middleware required: Whether you push canonical Schema v1 or point our Pull Connector at your existing PMS JSON feed, Plannyfy normalizes your offerings and attaches rich media automatically.
Real-Time Outbound Webhooks · HMAC-SHA256

Interactive Webhook Event & Signature Inspector

Click any of the 4 live webhook events below to inspect the X-Plannyfy-Signature header, the exact JSON payload delivered to your endpoint, and the Node.js verification snippet.

POSThttps://api.yourhotel.com/webhooks/plannyfy
200 OK · 118ms · TLS 1.3
X-Plannyfy-Signature: t=1792749600,v1=94f8a2b1c703e841f92d65a0b3e7c118294a6f0d1e8c3b2a5f9d7e6c4b1a0f2e
{
  "id": "evt_opp_99281a",
  "version": "v1",
  "event": "opportunity.matched",
  "createdAt": "2026-10-23T14:20:00.000Z",
  "vendorId": "vendor-villa-belvedere",
  "data": {
    "tripId": "trip-italy-anniversary",
    "title": "10-Day Rome, Florence & Amalfi Coast Anniversary Escape",
    "destination": "Rome, Florence & Amalfi Coast, Italy",
    "travelers": 2,
    "priority": "value",
    "marketBenchmarkUsd": 9700,
    "matchedPortfolioItems": [
      {
        "externalId": "belvedere-positano-01",
        "title": "Villa Belvedere Positano · Sea-View Terrace Suite",
        "suggestedBidUsd": 2720
      }
    ],
    "biddingClosesAt": "2026-10-30T14:20:00.000Z"
  }
}
Verifying `X-Plannyfy-Signature` (Node.js)

Every outbound webhook is signed using your private webhookSecret. Compute the HMAC-SHA256 digest over ${t}.${rawBody} and compare with constant-time equality.

import { createHmac, timingSafeEqual } from "crypto";

/** rawBody must be the exact bytes Plannyfy sent, before JSON.parse. */
export function verifyPlannyfyWebhook(rawBody: string, signatureHeader: string, secret: string, toleranceSec = 300): boolean {
  const parts = Object.fromEntries(signatureHeader.split(",").map((kv) => kv.trim().split("=")));
  const t = Number(parts.t);
  if (!Number.isFinite(t) || !parts.v1) return false;
  if (Math.abs(Date.now() / 1000 - t) > toleranceSec) return false; // too old: replay
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
  const given = Buffer.from(parts.v1, "hex");
  return given.length === expected.length && timingSafeEqual(given, expected);
}
Live Loopback Tester Included: You can trigger signed test webhooks directly from your Vendor Portfolio Studio or via POST /api/v1/vendor/webhooks/test.

Authentication & limits

API keys, scopes and rate limits

Create keys in the Plannyfy app under Account > API keys. A key only ever sees your own account's data.

Authentication

Send the key on every request. The secret is shown once, when you create it; Plannyfy keeps only a hash, so a lost key is replaced, not recovered.

Authorization: Bearer pfy_live_YOUR_KEY
# or
X-Plannyfy-Api-Key: pfy_live_YOUR_KEY

pfy_test_ keys belong to demo accounts and only work on demo data.

Errors

Errors are JSON with a machine-readable code, a message and a request id. Details, every code and retry advice are in the Conventions section below.

HTTP 403
{ "ok": false,
  "error": {
    "code": "insufficient_scope",
    "message": "This API key needs the `bids:write` scope.",
    "requestId": "req_3fa85f6457174562"
  } }
Rate limits and tiers

Limits are per key, and the day resets at 00:00 UTC. Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (Unix seconds). At the limit you get 429 with Retry-After.

TierPer minutePer day
Free605,000
Pro600100,000
EnterpriseCustom limits for your key

Need more? Email concierge@plannyfy.com.

Scopes

A key can only call endpoints whose scope it holds. Each endpoint below lists its scope.

  • portfolio:readRead your offerings.
  • portfolio:writeCreate, update and delete your offerings.
  • photos:readRead your photo library and the Plannyfy gallery.
  • photos:writeUpload, edit and delete your photos.
  • collections:readRead your photo collections.
  • collections:writeCreate, edit, duplicate and delete your photo collections.
  • subvendors:readRead your sub-vendor invites and synced offerings.
  • subvendors:writePush sub-vendor offerings and manage invites.
  • webhooks:testSend a signed test webhook to your endpoint.
  • billing:readRead your saved payment methods (never full card numbers).
  • escrow:writeStart an escrow checkout for one of your trips.
  • opportunities:readRead the open trips and events that match your services.
  • bids:readRead your own bids.
  • bids:writePlace, revise and save drafts of your own bids.
  • earnings:readRead your won bookings, commission and net earnings.
  • payouts:readRead the state of your payouts.
  • webhooks:readRead your webhook endpoint settings and recent deliveries (the secret is never returned).
  • webhooks:writeChange your webhook endpoint and events, and rotate the webhook signing secret.
  • availability:readRead your availability calendar, blackout windows and daily capacity limits.
  • availability:writeUpdate your blackout date ranges, minimum notice and daily capacity limits.
  • trips:readRead your own trips, items, live optimal mix picks and escrow state.
  • trips:writeCreate, publish and edit your own trips or event packages.
  • messages:readRead moderated concierge and Q&A messages on trips you own or bid on.
  • messages:writeSend moderated concierge and Q&A messages on trips you own or bid on.

Conventions · v1.7.0

Errors, retries, paging, webhooks and changes

The same rules apply to every /api/v1 endpoint. The machine-readable version is the OpenAPI 3.1 document.

Errors

One shape for every error. Branch on error.code, never on the message. Every response, success or error, carries X-Request-Id (the same value as error.requestId): quote it when you contact support.

HTTP 409
X-Request-Id: req_3fa85f6457174562
{ "ok": false,
  "error": {
    "code": "bid_exists",
    "message": "You already have a live bid on this trip. ...",
    "requestId": "req_3fa85f6457174562"
  },
  "bidId": "bid-1042" }

Some errors add fields next to error (bidId, needsConfirm, errors). Codes:

unauthenticatedinvalid_keykey_revokedkey_expiredinsufficient_scopeforbidden_rolesession_onlycross_siterate_limitedbad_requestvalidation_failednot_foundconflictpayload_too_largeunsupported_media_typeidempotency_key_invalididempotency_key_reusedidempotency_in_progressvendor_not_approvedbid_existsbid_expiredtrip_closedtrip_changedconfirmation_requiredin_usebuilt_ininvalidtoo_largenot_an_imageunreadablecap_reachedstorage_unavailableservice_unavailableinternal_error

Statuses: 400 malformed, 401 key problem, 403 scope or account type, 404 not found or not yours, 409 state conflict, 413 too large, 422 breaks a rule, 429 rate limited (see Retry-After), 5xx ours: retry with backoff.

Idempotency

Send Idempotency-Key (a UUID works well) on any POST so a timeout or retry cannot do the work twice. The first answer is kept for 24 hours, per key owner.

  • Same key, same method, path and body: the stored answer comes back with Idempotent-Replayed: true. Nothing runs again.
  • Same key, anything different: 409 idempotency_key_reused. Use a new key for a different request.
  • First request still running: 409 idempotency_in_progress with Retry-After: 1.
  • 5xx and 429 answers are not kept, so retrying them is safe and simple.
  • Rotating a webhook secret ignores the key on purpose: the secret is never stored in an answer.
curl -X POST "https://plannyfy.com/api/v1/vendor/bids" \
  -H "Authorization: Bearer pfy_live_YOUR_KEY" \
  -H "Idempotency-Key: 7f1b8f0e-1d1c-4c4e-8a77-0a4f6a1f5c2e" \
  -H "Content-Type: application/json" -d '{ ... }'
Caching, CORS and size
  • Every response is Cache-Control: no-store.
  • There is no CORS: call the API from your servers. A key must never be shipped to a browser.
  • JSON bodies up to 2 MB (413 payload_too_large); photo uploads up to 25 MB.
  • Rate limits and tiers are in the Authentication section above.
Webhooks: events, delivery and retries

Plannyfy sends a signed POST with a JSON body to your endpoint. Set it with PUT /api/v1/vendor/webhooks, test it with POST /api/v1/vendor/webhooks/test.

  • opportunity.matched · Fired when a traveler publishes a trip whose destination & items match your portfolio.
  • bid.status_changed · Fired when your live bid becomes Contested, Outbid, or Paused after a traveler trip edit.
  • trip.locked · Fired when a traveler locks in a trip and your lines win (includes escrow allocation payload).
  • dispute.updated · Fired when a post-lock issue or withdrawal review is opened or resolved.
POST https://api.yourhotel.com/webhooks/plannyfy
Content-Type: application/json
X-Plannyfy-Event: trip.locked
X-Plannyfy-Signature: t=1792798000,v1=5a8c91d4...
User-Agent: Plannyfy-Webhooks/1.0

{ "id": "evt_mh3k2x9a_1f2e3d4c", "version": "v1", "event": "trip.locked",
  "vendorId": "vendor-atlas-dmc", "createdAt": "2026-10-26T06:00:00.000Z",
  "data": { ... } }
  • Delivery: one attempt with a 3.5 second timeout; any 2xx means received.
  • Retries: there is no automatic retry yet. Treat a webhook as a hint and reconcile with GET /vendor/opportunities, /bids and /payouts.
  • Log: the last 20 deliveries and the status your endpoint answered are in GET /api/v1/vendor/webhooks.
  • Duplicates: de-duplicate on the envelope id.
  • Privacy: payloads never contain a traveler's budget, per-item targets, an Ask Realism Score or a rival's price.
Verify the signature

The header is t=<unix_seconds>,v1=<hex>: HMAC-SHA256 with your webhook secret over <t>.<raw body>. Verify over the raw bytes before parsing, compare in constant time, and reject timestamps more than 5 minutes old.

Node.js
import { createHmac, timingSafeEqual } from "crypto";

/** rawBody must be the exact bytes Plannyfy sent, before JSON.parse. */
export function verifyPlannyfyWebhook(rawBody: string, signatureHeader: string, secret: string, toleranceSec = 300): boolean {
  const parts = Object.fromEntries(signatureHeader.split(",").map((kv) => kv.trim().split("=")));
  const t = Number(parts.t);
  if (!Number.isFinite(t) || !parts.v1) return false;
  if (Math.abs(Date.now() / 1000 - t) > toleranceSec) return false; // too old: replay
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
  const given = Buffer.from(parts.v1, "hex");
  return given.length === expected.length && timingSafeEqual(given, expected);
}
Python
import hmac, hashlib, time

def verify_plannyfy_webhook(raw_body: bytes, signature_header: str, secret: str, tolerance_sec: int = 300) -> bool:
    parts = dict(kv.strip().split("=", 1) for kv in signature_header.split(","))
    try:
        t = int(parts["t"]); given = parts["v1"]
    except (KeyError, ValueError):
        return False
    if abs(time.time() - t) > tolerance_sec:
        return False  # too old: replay
    expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(given, expected)

Rotate the secret with POST /api/v1/vendor/webhooks/rotate-secret: the new secret is shown once and the old one stops signing at once.

Versioning and changelog

The version in the path is the major version (v1). Adding endpoints, fields, scopes and error codes does not change it, so ignore fields you do not know. A breaking change would ship under a new path version with notice here. The document version is 1.7.0 and is also in /api/health.

1.7.02026-10-26Customer trips, package lock, completion sign-off, disputes, vendor availability, bid confirm/withdraw & moderated concierge messaging
  • New: GET and POST /api/v1/trips, GET and PATCH /api/v1/trips/{id} so customers can create, publish, edit, and inspect trips and their live #1 Best Price / #1 Best Value optimal mix via API.
  • New: POST /api/v1/trips/{id}/lock, /complete, /cancel, and /disputes for full customer escrow lock-in, post-trip completion sign-off, policy cancellations, and dispute creation.
  • New: POST /api/v1/vendor/bids/{id}/withdraw and POST /api/v1/vendor/bids/{id}/confirm for vendor bid withdrawal and mid-bid customer-edit price re-confirmation.
  • New: GET and PUT /api/v1/vendor/availability for vendor capacity caps (maxEventsPerDay, leadTimeDays) and blackout date ranges.
  • New: GET and POST /api/v1/trips/{id}/messages for moderated concierge messaging between customers and bidding/locked vendors (with PII/contact-info blocking).
  • New scopes: trips:read, trips:write, availability:read, availability:write, messages:read, messages:write.
1.6.02026-10-26Bids, earnings, payouts, webhook settings and standard behaviors
  • New: GET /api/v1/vendor/opportunities and /opportunities/{id} (matched, traveler-safe, event fields, offering candidates).
  • New: GET and POST /api/v1/vendor/bids, GET and PATCH /api/v1/vendor/bids/{id} with the bid composer's rules, drafts, unit pricing, minimum guests and offering links by offeringId.
  • New: GET /api/v1/vendor/earnings and /payouts.
  • New: GET and PUT /api/v1/vendor/webhooks and POST /api/v1/vendor/webhooks/rotate-secret; webhook events, payloads, signature and delivery behavior are documented.
  • New scopes: opportunities:read, bids:read, bids:write, earnings:read, payouts:read, webhooks:read, webhooks:write. Existing keys keep working with the scopes they have.
  • Changed: every error is now { ok: false, error: { code, message, requestId } }. Before, error was a string and code sat next to it. Read error.code and error.message.
  • New: X-Request-Id on every response, Cache-Control: no-store on every response, Idempotency-Key on every POST (24 hours, per key owner), and the new error codes listed in the OpenAPI document.
  • New: limit and cursor pagination with a next link; GET /api/v1/vendor/portfolio now pages (default 200, up to 500 per page).
  • Changed: JSON request bodies are limited to 2 MB (413 payload_too_large); the test webhook only accepts a public https webhookUrl.
1.5.0earlierScoped keys, tiers, photos, collections, escrow checkout
  • API keys with scopes and Free, Pro and Enterprise rate limits; X-RateLimit-* headers and Retry-After.
  • Photo library, collections and the Plannyfy gallery endpoints; escrow checkout and payment-method reads for customers.
  • Earlier history was not recorded in a changelog.

Complete Single-Page Reference · v1.7.0

Plannyfy API Endpoints & Schema Reference

Every public Plannyfy v1.7.0 endpoint on one page—grouped clearly with parameters, request snippets, and JSON responses. Use the search bar below to filter by endpoint, parameter, or keyword.

Group 1 · Portfolio, Inventory & Sub-Vendors

Portfolio, Inventory & Sub-Vendor Network API

List, upsert, and delete hotel, villa, yacht, tour, guide, and transfer offerings in Plannyfy Common Schema v1, plus manage sub-vendor onboarding links and wholesale offerings.

GET/api/v1/vendor/portfolioList & Filter Portfolio Offerings
Bearer pfy_live_... · scope portfolio:read

Returns all portfolio offerings for the authenticated vendor in Plannyfy Common Schema v1 (`plannyfy.travel.portfolio.v1`), optionally filtered by category.

Parameters & Fields
category
queryoptional
"hotel" | "activity" | "guide" | "transfer" | "train" | "flight"

Optional filter to return only offerings in a specific category.

Example Request (cURL)
curl -X GET "https://plannyfy.com/api/v1/vendor/portfolio?category=hotel" \
  -H "Authorization: Bearer pfy_live_YOUR_KEY"
Example Response200 OK
{
  "ok": true,
  "version": "v1",
  "schema": "plannyfy.travel.portfolio.v1",
  "vendorId": "demo-vendor",
  "count": 1,
  "items": [
    {
      "id": "pf-rome-palazzo",
      "externalId": "pal-roma-01",
      "category": "hotel",
      "title": "Hotel Palazzo Roma · 5★ Boutique Stay",
      "location": "Rome, Italy",
      "pricingUnit": "per_night",
      "basePriceUsd": 420,
      "seasonalRates": { "peakSummerUsd": 510, "shoulderUsd": 420, "lowSeasonUsd": 350 },
      "floorPctOfBase": 84,
      "includedPerks": ["Daily breakfast included", "Welcome Prosecco"]
    }
  ]
}
POST/api/v1/vendor/portfolioUpsert 1–100 Portfolio Offerings
Bearer pfy_live_... · scope portfolio:write

Idempotently creates or updates 1–100 portfolio offerings per request. Accepts canonical Plannyfy Schema v1 (`{ items: [...] }`) as well as external PMS field aliases (`sku`, `name`, `rate`, `photos`, `perks`).

Parameters & Fields
title
bodyrequired
string (alias: name)

Offering title displayed to customers (max 140 chars).

basePriceUsd
bodyrequired
number (alias: rate, price)

Standard net unit rate in USD.

externalId
bodyoptional
string (alias: sku, code)

Partner SKU used for idempotent upserts across syncs.

seasonalRates
bodyoptional
{ peakSummerUsd?, shoulderUsd?, lowSeasonUsd? }

Optional Peak (Jun–Aug), Shoulder (Apr–May, Sep–Oct), and Low (Nov–Mar) rates.

galleryUrls / youtubeUrl
bodyoptional
string[] / string

Multi-photo gallery URLs and 4K YouTube walkthrough URL.

Example Request (cURL)
curl -X POST "https://plannyfy.com/api/v1/vendor/portfolio" \
  -H "Authorization: Bearer pfy_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "version": "v1",
    "items": [
      {
        "externalId": "pal-roma-01",
        "category": "hotel",
        "title": "Hotel Palazzo Roma · 5★ Boutique Stay",
        "location": "Rome, Italy",
        "pricingUnit": "per_night",
        "basePriceUsd": 420,
        "seasonalRates": { "peakSummerUsd": 510, "shoulderUsd": 420, "lowSeasonUsd": 350 },
        "floorPctOfBase": 84,
        "includedPerks": ["Daily breakfast included", "Welcome Prosecco"]
      }
    ]
  }'
Example Response200 OK
{
  "ok": true,
  "version": "v1",
  "schema": "plannyfy.travel.portfolio.v1",
  "vendorId": "demo-vendor",
  "upsertedCount": 1,
  "items": [
    {
      "id": "pf-demo-vendor-pal-roma-01",
      "externalId": "pal-roma-01",
      "title": "Hotel Palazzo Roma · 5★ Boutique Stay",
      "basePriceUsd": 420,
      "source": "api_push"
    }
  ]
}
DELETE/api/v1/vendor/portfolioDelete a Portfolio Offering
Bearer pfy_live_... · scope portfolio:write

Removes a portfolio offering from your catalog by Plannyfy item `id` or your internal `externalId` (SKU).

Parameters & Fields
externalId
queryoptional
string

Partner external SKU / room code (e.g. `pal-roma-01`). Provide either `externalId` or `id`.

id
queryoptional
string

Plannyfy portfolio item ID (e.g. `pf-rome-palazzo`).

Example Request (cURL)
curl -X DELETE "https://plannyfy.com/api/v1/vendor/portfolio?externalId=pal-roma-01" \
  -H "Authorization: Bearer pfy_live_YOUR_KEY"
Example Response200 OK
{
  "ok": true,
  "version": "v1",
  "vendorId": "demo-vendor",
  "deletedId": "pf-rome-palazzo"
}
GET/api/v1/vendor/sub-vendorsList Sub-Vendor Invite Links & Wholesale Offerings
Bearer pfy_live_... · scope subvendors:read

Lists all sub-vendor onboarding invite links created by the primary vendor/DMC along with synced sub-vendor wholesale offerings and auto-calculated markup rates (`effectiveBidUsd`).

Parameters & Fields
Authorization
headeroptional
Bearer pfy_live_...

Primary vendor API key (`X-Plannyfy-Api-Key`) or authenticated vendor session cookie.

Example Request (cURL)
curl -X GET "https://plannyfy.com/api/v1/vendor/sub-vendors" \
  -H "Authorization: Bearer pfy_live_YOUR_KEY"
Example Response200 OK
{
  "ok": true,
  "version": "v1",
  "vendorId": "demo-vendor",
  "inviteCount": 2,
  "invites": [
    {
      "id": "svi-capri-yacht",
      "subVendorName": "Capri Riva Private Yachts",
      "category": "activity",
      "location": "Capri & Amalfi Coast, Italy",
      "defaultMarkupPct": 15,
      "status": "active",
      "inviteUrl": "/vendor/invite/svi-capri-yacht"
    }
  ],
  "subVendorOfferingCount": 2,
  "subVendorOfferings": [
    {
      "id": "pf-sub-capri-yacht",
      "title": "Private Riva Yacht Full-Day Capri Charter",
      "wholesaleRateUsd": 1400,
      "markupPct": 15,
      "effectiveBidUsd": 1610
    }
  ]
}
POST/api/v1/vendor/sub-vendorsCreate Sub-Vendor Invite or Push Wholesale Offering
Bearer pfy_live_... · scope subvendors:write

Creates a scoped sub-vendor onboarding link (`action: "create_invite"`) or pushes a sub-vendor wholesale offering (`action: "push_offering"`) that automatically computes `basePriceUsd = round(wholesaleRateUsd * (1 + markupPct / 100))` in the primary vendor's portfolio.

Parameters & Fields
action
bodyoptional
"create_invite" | "push_offering"

Selects whether to generate a new sub-vendor invite link or push a sub-vendor wholesale rate.

subVendorName / subVendorEmail
bodyoptional
string

Required for `create_invite`: partner business name and contact email.

defaultMarkupPct / markupPct
bodyoptional
number (0–60)

Wholesale-to-bid markup percentage applied when composing primary vendor bids.

inviteId / wholesaleRateUsd / title
bodyoptional
string / number / string

Required for `push_offering`: sub-vendor invite token, net wholesale rate in USD, and offering title.

Example Request (cURL)
# 1. Primary vendor creates a sub-vendor onboarding link
curl -X POST "https://plannyfy.com/api/v1/vendor/sub-vendors" \
  -H "Authorization: Bearer pfy_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "create_invite",
    "subVendorName": "Sorrento Limo & Mercedes Fleet",
    "subVendorEmail": "dispatch@sorrentolimo.it",
    "category": "transfer",
    "location": "Naples & Amalfi Coast, Italy",
    "defaultMarkupPct": 15
  }'

# 2. Sub-vendor pushes a wholesale offering via inviteId
curl -X POST "https://plannyfy.com/api/v1/vendor/sub-vendors" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "push_offering",
    "inviteId": "svi-sorrento-transfers",
    "title": "Mercedes V-Class Private Transfer Naples ↔ Positano",
    "wholesaleRateUsd": 220,
    "pricingUnit": "per_group",
    "includedPerks": ["English-speaking chauffeur", "Flight tracking"]
  }'
Example Response200 OK
{
  "ok": true,
  "version": "v1",
  "parentVendorId": "demo-vendor",
  "item": {
    "id": "pf-sub-svi-sorrento-transfers",
    "title": "Mercedes V-Class Private Transfer Naples ↔ Positano",
    "wholesaleRateUsd": 220,
    "subVendorMarkupPct": 15,
    "basePriceUsd": 253,
    "source": "sub_vendor"
  }
}
Group 2 · Webhooks & Events

Outbound Webhooks & Verification API

Trigger signed HMAC-SHA256 test webhook deliveries and verify real-time event notifications (`X-Plannyfy-Signature`).

POST/api/v1/vendor/webhooks/testTrigger Signed Test Webhook Delivery
Bearer pfy_live_... · scope webhooks:test

Generates a realistic event payload, signs it with your `webhookSecret` (`X-Plannyfy-Signature: t=<unix_sec>,v1=<hmac_sha256>`), dispatches it to your `webhookUrl`, and records the delivery in your log.

Parameters & Fields
event
bodyoptional
"opportunity.matched" | "bid.status_changed" | "trip.locked" | "dispute.updated"

Webhook lifecycle event to simulate (defaults to `opportunity.matched`).

webhookUrl
bodyoptional
string (HTTPS URL)

Optional override destination URL for testing before saving.

Example Request (cURL)
curl -X POST "https://plannyfy.com/api/v1/vendor/webhooks/test" \
  -H "Authorization: Bearer pfy_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "event": "opportunity.matched",
    "webhookUrl": "https://api.yourhotel.com/webhooks/plannyfy"
  }'
Example Response200 OK
{
  "ok": true,
  "event": "opportunity.matched",
  "webhookUrl": "https://api.yourhotel.com/webhooks/plannyfy",
  "signatureHeader": "X-Plannyfy-Signature: t=1792798000,v1=5a8c91d4f20e7b11a3e84c9b2016d7f9c4e12a8b",
  "statusText": "200 OK · Signed \"opportunity.matched\" delivered to https://api.yourhotel.com/webhooks/plannyfy",
  "payload": { "id": "evt_...", "version": "v1", "event": "opportunity.matched", "vendorId": "...", "createdAt": "...", "data": { } },
  "delivery": {
    "id": "whd-m1x2-ab12",
    "event": "opportunity.matched",
    "mode": "test",
    "webhookUrl": "https://api.yourhotel.com/webhooks/plannyfy",
    "statusCode": 200,
    "statusText": "200 OK · ...",
    "deliveredAt": "2026-10-26T06:00:00.000Z"
  }
}
GET/api/v1/vendor/webhooksRead webhook settings
Bearer pfy_live_... · scope webhooks:read

Your endpoint URL, subscribed events, the last 20 deliveries (with the HTTP status your endpoint answered) and the events that exist. The signing secret is never returned.

Parameters & Fields
Example Request (cURL)
curl -X GET "https://plannyfy.com/api/v1/vendor/webhooks" \
  -H "Authorization: Bearer pfy_live_YOUR_KEY"
Example Response200 OK
{
  "ok": true,
  "version": "v1",
  "webhook": { "url": "https://api.yourhotel.com/webhooks/plannyfy", "events": ["opportunity.matched", "trip.locked"], "secretHint": "whsec_…9c1d", "lastTestAt": null, "lastTestStatus": null, "updatedAt": "2026-10-26T06:00:00.000Z" },
  "deliveries": [{ "id": "whd-m1x2-ab12", "event": "trip.locked", "mode": "live", "statusCode": 200, "statusText": "200 OK ...", "deliveredAt": "2026-10-26T05:41:00.000Z" }],
  "availableEvents": [{ "id": "opportunity.matched", "description": "..." }]
}
PUT/api/v1/vendor/webhooksChange webhook settings
Bearer pfy_live_... · scope webhooks:write

Sets the endpoint URL and/or the subscribed events; what you leave out stays. url: null removes the endpoint. The URL must be https and a public host (no credentials, localhost, private or link-local address). The first time an endpoint is set, a signing secret is created: fetch it with rotate-secret.

Parameters & Fields
url
bodyoptional
string (https) | null

Your receiving endpoint.

events
bodyoptional
string[]

opportunity.matched, bid.status_changed, trip.locked, dispute.updated.

Example Request (cURL)
curl -X PUT "https://plannyfy.com/api/v1/vendor/webhooks" \
  -H "Authorization: Bearer pfy_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://api.yourhotel.com/webhooks/plannyfy",
    "events": [
      "opportunity.matched",
      "trip.locked"
    ]
  }'
Example Response200 OK
{ "ok": true, "version": "v1", "webhook": { "url": "https://api.yourhotel.com/webhooks/plannyfy", "events": ["opportunity.matched", "trip.locked"], "secretHint": "whsec_…9c1d" } }
POST/api/v1/vendor/webhooks/rotate-secretRotate the webhook signing secret
Bearer pfy_live_... · scope webhooks:write

Replaces your signing secret and returns the new one, this once. The old secret stops signing at once: update your receiver first, or accept both for a while. Not idempotent; Idempotency-Key is ignored here on purpose so the secret is never kept in a stored answer.

Parameters & Fields
Example Request (cURL)
curl -X POST "https://plannyfy.com/api/v1/vendor/webhooks/rotate-secret" \
  -H "Authorization: Bearer pfy_live_YOUR_KEY"
Example Response200 OK
{ "ok": true, "version": "v1", "webhookSecret": "whsec_plannyfy_a1b2c3d4_…", "rotatedAt": "2026-10-26T06:10:00.000Z", "note": "Shown once. Store it now." }
Group 3 · Photos & Collections

Photo Library, Collections & Gallery API

Upload photos to your library, group them into collections that your offerings link to, and browse the Plannyfy stock gallery. Every call only sees your own library.

GET/api/v1/vendor/photosList your photos
Bearer pfy_live_... · scope photos:read

Your photo library, newest first. Each photo has an `id`, a `url`, its `alt` text, its `tags` and size. Search and filter by tag; send `page` or `limit` to page the results.

Parameters & Fields
q
queryoptional
string

Search alt text, file name and tags.

tag
queryoptional
string

Filter by tag. Repeat or comma-separate: a photo must carry every tag.

sort
queryoptional
string

`newest` (default), `oldest` or `name`.

page
queryoptional
integer

Page number. Paging starts when `page` or `limit` is sent.

limit
queryoptional
integer

Photos per page, up to 100.

Example Request (cURL)
curl "https://plannyfy.com/api/v1/vendor/photos" \
  -H "Authorization: Bearer pfy_live_YOUR_KEY"
Example Response200 OK
{
  "ok": true,
  "photos": [{ "id": "a1b2c3d4e5f60718293a", "url": "https://plannyfy.com/api/media/...", "alt": "Rooftop terrace", "tags": ["rooftop", "suites"] }],
  "tags": [{ "tag": "rooftop", "count": 4 }]
}
POST/api/v1/vendor/photosUpload a photo
Bearer pfy_live_... · scope photos:write

Send one image as `multipart/form-data`. It is converted to WebP and its metadata is removed. Answers `413` when too large, `415` when it is not an image, `409` at the library cap.

Parameters & Fields
file
bodyrequired
file (multipart)

The image to add to your library.

tags
bodyoptional
string

Comma-separated tags for the photo (up to 10, lowercase, 24 characters each).

Example Request (cURL)
curl -X POST "https://plannyfy.com/api/v1/vendor/photos" \
  -H "Authorization: Bearer pfy_live_YOUR_KEY" \
  -F "file=@terrace.jpg"
Example Response201 Created
{ "ok": true, "photo": { "id": "a1b2c3d4e5f60718293a", "url": "https://plannyfy.com/api/media/...", "alt": "" } }
POST/api/v1/vendor/photos/bulkChange several photos
Bearer pfy_live_... · scope photos:write

Tag, untag, add to a collection or delete up to 100 photos at once. You get a result per photo (`done`, `unchanged` or `failed`), so one failure never blocks the rest and repeating a request is safe. Deleting keeps photos that offerings still show (`in_use`) unless you send `force`.

Parameters & Fields
action
bodyrequired
string

`add_tags`, `remove_tags`, `add_to_collection` or `delete`.

ids
bodyrequired
string[]

Photo ids, up to 100.

tags
bodyoptional
string[]

For `add_tags` and `remove_tags`.

collectionId
bodyoptional
string

For `add_to_collection`.

force
bodyoptional
boolean

For `delete`: also delete photos that offerings show.

Example Request (cURL)
curl -X POST "https://plannyfy.com/api/v1/vendor/photos/bulk" \
  -H "Authorization: Bearer pfy_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "action": "add_tags", "ids": ["a1b2c3d4e5f60718293a"], "tags": ["rooftop"] }'
Example Response200 OK
{ "ok": true, "results": [{ "id": "a1b2c3d4e5f60718293a", "status": "done" }], "summary": { "done": 1, "unchanged": 0, "failed": 0 } }
GET/api/v1/vendor/galleryList Plannyfy gallery photos
Bearer pfy_live_... · scope photos:read

The active Plannyfy stock photos you can add to a collection, newest first, plus `collections`: curated Plannyfy sets whose `photoKeys` you can add all at once.

Parameters & Fields
Example Request (cURL)
curl "https://plannyfy.com/api/v1/vendor/gallery" \
  -H "Authorization: Bearer pfy_live_YOUR_KEY"
Example Response200 OK
{ "ok": true, "photos": [{ "key": "rome", "url": "https://plannyfy.com/api/media/...", "label": "Rome" }] }
GET/api/v1/vendor/collectionsList your photo collections
Bearer pfy_live_... · scope collections:read

Your collections with their `tags`, photos resolved (cover first) and the offerings that use each. Search and filter by tag like the photo list.

Parameters & Fields
q
queryoptional
string

Search names and tags.

tag
queryoptional
string

Filter by tag (repeat or comma-separate; all must match).

sort
queryoptional
string

`updated` (default), `name` or `photos`.

page
queryoptional
integer

Page number. Paging starts when `page` or `limit` is sent.

limit
queryoptional
integer

Collections per page, up to 100.

Example Request (cURL)
curl "https://plannyfy.com/api/v1/vendor/collections" \
  -H "Authorization: Bearer pfy_live_YOUR_KEY"
Example Response200 OK
{ "ok": true, "collections": [{ "id": "c1", "name": "Palazzo suites", "photos": [], "usedBy": [] }] }
POST/api/v1/vendor/collectionsCreate a photo collection
Bearer pfy_live_... · scope collections:write

Create a collection from library photos or gallery photos, or send `fromOfferingId` to save an offering's current photos as a collection and link the offering to it.

Parameters & Fields
name
bodyrequired
string

Collection name.

items
bodyoptional
{ ref, alt?, caption? }[]

Photos in order. `ref` is `{ kind: "library", photoId }` or `{ kind: "plannyfy", key }`.

tags
bodyoptional
string[]

Up to 10 tags (lowercase, 24 characters each).

fromOfferingId
bodyoptional
string

Create the collection from one of your offerings' photos instead.

Example Request (cURL)
curl -X POST "https://plannyfy.com/api/v1/vendor/collections" \
  -H "Authorization: Bearer pfy_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Palazzo suites", "items": [{ "ref": { "kind": "library", "photoId": "a1b2c3d4e5f60718293a" } }] }'
Example Response201 Created
{ "ok": true, "id": "c1", "name": "Palazzo suites", "photos": [], "skipped": [] }
GET/api/v1/vendor/collections/{id}Get a collection
Bearer pfy_live_... · scope collections:read

One of your photo collections with its resolved photos and the offerings that use it. Someone else's collection is the same 404 as a missing one.

Parameters & Fields
id
queryrequired
string (path)

The collection id.

Example Request (cURL)
curl -X GET "https://plannyfy.com/api/v1/vendor/collections/a1b2c3d4e5f60718293a" \
  -H "Authorization: Bearer pfy_live_YOUR_KEY"
Example Response200 OK
{ "ok": true, "collection": { "id": "a1b2c3d4e5f60718293a", "name": "Palazzo suites", "tags": ["rome"] }, "photos": [], "usedBy": [] }
PATCH/api/v1/vendor/collections/{id}Edit a collection
Bearer pfy_live_... · scope collections:write

Same body as creating one. Every offering linked to the collection shows the change.

Parameters & Fields
name
bodyrequired
string

Collection name.

items[]
bodyrequired
{ ref: { kind, photoId | key }, alt?, caption? }

The photos in order.

coverPhotoRef / tags
bodyoptional
ref / string[]

Cover photo and tags.

Example Request (cURL)
curl -X PATCH "https://plannyfy.com/api/v1/vendor/collections/a1b2c3d4e5f60718293a" \
  -H "Authorization: Bearer pfy_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Palazzo suites",
    "items": [
      {
        "ref": {
          "kind": "library",
          "photoId": "ph_123"
        }
      }
    ]
  }'
Example Response200 OK
{ "ok": true, "collection": { "id": "a1b2c3d4e5f60718293a", "name": "Palazzo suites" }, "photos": [], "usedBy": [] }
DELETE/api/v1/vendor/collections/{id}Delete a collection
Bearer pfy_live_... · scope collections:write

409 in_use (with usedBy) while offerings show the collection. Add ?force=1 to delete anyway: those offerings keep their photos as a plain gallery.

Parameters & Fields
force
queryoptional
1

Delete even when offerings use it.

Example Request (cURL)
curl -X DELETE "https://plannyfy.com/api/v1/vendor/collections/a1b2c3d4e5f60718293a?force=1" \
  -H "Authorization: Bearer pfy_live_YOUR_KEY"
Example Response200 OK
{ "ok": true }
POST/api/v1/vendor/collections/{id}/duplicateDuplicate a collection
Bearer pfy_live_... · scope collections:write

An independent copy named "<name> (copy)".

Parameters & Fields
Idempotency-Key
headeroptional
string

Makes a retry safe.

Example Request (cURL)
curl -X POST "https://plannyfy.com/api/v1/vendor/collections/a1b2c3d4e5f60718293a/duplicate" \
  -H "Authorization: Bearer pfy_live_YOUR_KEY" \
  -H "Idempotency-Key: 7f1b8f0e-1d1c-4c4e-8a77-0a4f6a1f5c2e"
Example Response201 Created
{ "ok": true, "collection": { "id": "b2c3d4e5f60718293a4b", "name": "Palazzo suites (copy)" }, "photos": [], "usedBy": [] }
PATCH/api/v1/vendor/photos/{id}Edit a photo
Bearer pfy_live_... · scope photos:write

Change the alt text and/or tags of one of your photos (send at least one).

Parameters & Fields
alt
bodyoptional
string

Alt text.

tags
bodyoptional
string[]

Replaces the photo's tags.

Example Request (cURL)
curl -X PATCH "https://plannyfy.com/api/v1/vendor/photos/ph_123" \
  -H "Authorization: Bearer pfy_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "alt": "Rooftop terrace at dusk",
    "tags": [
      "rome",
      "terrace"
    ]
  }'
Example Response200 OK
{ "ok": true, "photo": { "id": "ph_123", "alt": "Rooftop terrace at dusk", "tags": ["rome", "terrace"] } }
DELETE/api/v1/vendor/photos/{id}Delete a photo
Bearer pfy_live_... · scope photos:write

409 in_use (with usedBy) while offerings show the photo. Add ?force=1 to delete anyway and remove it from those offerings.

Parameters & Fields
force
queryoptional
1

Delete even when offerings use it.

Example Request (cURL)
curl -X DELETE "https://plannyfy.com/api/v1/vendor/photos/ph_123?force=1" \
  -H "Authorization: Bearer pfy_live_YOUR_KEY"
Example Response200 OK
{ "ok": true }
Group 4 · Escrow & Billing

Plannyfy Escrow & Billing API

Start escrow checkout sessions for your own trips and read your saved payment methods. Saving or changing payment settings is only available in the Plannyfy app.

POST/api/v1/escrow/checkoutInitialize Plannyfy Escrow Checkout & Payout Plan
Bearer pfy_live_... · scope escrow:write

Resolves the winning `price` or `value` allocation mix for a trip, calculates per-category platform commission and optional 100% fee-back trip protection, and creates the Plannyfy Escrow Checkout session and per-vendor payout plan.

Parameters & Fields
tripId
bodyrequired
string

ID of the open trip being locked into escrow.

pick
bodyoptional
"price" | "value"

Selected winning mix (`price` for Best Price or `value` for Best Value).

withInsurance
bodyoptional
boolean

When true, adds optional 100% fee-back cancellation protection.

paymentMethodId / paymentMethodType
bodyoptional
string / "card" | "ach_bank" | "express_wallet"

Selected or newly added customer payment method authorized for the 48h post-trip escrow hold.

Example Request (cURL)
curl -X POST "https://plannyfy.com/api/v1/escrow/checkout" \
  -H "Content-Type: application/json" \
  -d '{
    "tripId": "trip-italy-anniversary",
    "pick": "value",
    "withInsurance": true,
    "paymentMethodId": "pm-visa-4242",
    "paymentMethodType": "card",
    "paymentMethodLabel": "Visa Infinite •••• 4242"
  }'
Example Response200 OK
{
  "tripId": "trip-italy-anniversary",
  "pick": "value",
  "packageTotalUsd": 8820,
  "withInsurance": true,
  "insuranceFeeUsd": 353,
  "grandTotalUsd": 9173,
  "paymentMethodId": "pm-visa-4242",
  "paymentMethodType": "card",
  "paymentMethodLabel": "Visa Infinite •••• 4242",
  "totalPlatformFeeUsd": 1014,
  "blendedCommissionPct": 11.5,
  "vendorTransfers": [
    {
      "vendorId": "vendor-atlas-italia",
      "vendorName": "Atlas Italia Luxury DMC",
      "grossAmountUsd": 4950,
      "platformFeeUsd": 594,
      "netVendorPayoutUsd": 4356,
      "releaseStatus": "held_until_trip_start"
    }
  ]
}
POST/api/v1/billing/payment-methodsAdd Payment Method or Payout Account (App Session)
GET: Bearer pfy_live_... · scope billing:read. POST and PATCH: signed-in app only

Adds a customer payment method (`card`, `ach_bank`, `express_wallet`) or vendor payout receiving account (`bank_transfer`, `instant_debit`, `swift_wire`) from the signed-in Plannyfy web app. API keys receive `403 session_only`; use `GET /api/v1/billing/payment-methods` (`billing:read`) to list saved methods.

Parameters & Fields
role
bodyoptional
"customer" | "vendor"

Selects customer payment wallet (`card`, `ach_bank`, `express_wallet`) or vendor payout receiving rails (`bank_transfer`, `instant_debit`, `swift_wire`).

type / methodType
bodyrequired
"card" | "ach_bank" | "express_wallet" | "bank_transfer" | "instant_debit" | "swift_wire"

Payment method or payout receiving rail type.

Example Request (cURL)
curl -X POST "https://plannyfy.com/api/v1/billing/payment-methods" \
  -H "Content-Type: application/json" \
  -d '{
    "role": "customer",
    "type": "card",
    "brand": "Visa Infinite",
    "last4": "4242",
    "expiry": "08/28",
    "isDefault": true
  }'
Example Response200 OK
{
  "ok": true,
  "version": "v1",
  "role": "customer",
  "defaultPaymentMethodId": "pm-visa-4242"
}
GET/api/v1/billing/payment-methodsList payment methods
Bearer pfy_live_... · scope billing:read

A customer's saved payment methods (never full card numbers), or a vendor's payout receiving accounts and invoice settings.

Parameters & Fields
role
queryoptional
customer | vendor

Defaults to your account type.

Example Request (cURL)
curl -X GET "https://plannyfy.com/api/v1/billing/payment-methods" \
  -H "Authorization: Bearer pfy_live_YOUR_KEY"
Example Response200 OK
{ "ok": true, "version": "v1", "role": "customer", "defaultPaymentMethodId": "pm-visa-4242", "paymentMethods": [{ "id": "pm-visa-4242", "type": "card", "brand": "Visa", "last4": "4242", "expiry": "08/28", "isDefault": true }] }
PATCH/api/v1/billing/payment-methodsSwitch the payment method on an invoice
Signed-in app only

Changes the payment method attached to a customer escrow receipt or a vendor commission invoice.

Parameters & Fields
tripId / invoiceId
bodyrequired
string

The receipt (customer) or invoice (vendor) to change.

paymentMethodId
bodyoptional
string

The method to use.

Example Request (cURL)
curl -X PATCH "https://plannyfy.com/api/v1/billing/payment-methods" \
  -H "Authorization: Bearer pfy_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "tripId": "trip-italy-anniversary",
    "paymentMethodId": "pm-mc-8819"
  }'
Example Response200 OK
{ "ok": true, "version": "v1" }
Group 5 · Opportunities, Bids & Availability

Opportunities, Bids & Availability API

Find the open trips and events that match your services, place/revise/withdraw/confirm bids with server-side price limits, and manage your availability rules and blackout dates.

GET/api/v1/vendor/opportunitiesList matched opportunities
Bearer pfy_live_... · scope opportunities:read

Open trips and events that match your services and regions, best match first. Traveler-safe: a market benchmark and a competition band, never the traveler's budget, a target price or a rival's price. Events carry guestCount and sanitized eventDetails (no invoice note, no street address).

Parameters & Fields
matched
queryoptional
boolean (default true)

false lists every open opportunity, not only matches.

includeBid
queryoptional
boolean (default false)

Include opportunities you already have a live bid on.

packageType
queryoptional
travel | wedding | corporate_event | ...

Only this kind of opportunity.

category
queryoptional
string

Only trips with an item in this category (hotel, catering, ...).

q
queryoptional
string

Title or destination contains.

limit / cursor
queryoptional
integer 1-100 / string

Pagination: send page.nextCursor as cursor.

Example Request (cURL)
curl -X GET "https://plannyfy.com/api/v1/vendor/opportunities?matched=true&limit=20" \
  -H "Authorization: Bearer pfy_live_YOUR_KEY"
Example Response200 OK
{
  "ok": true,
  "version": "v1",
  "vendorId": "vendor-atlas-dmc",
  "opportunities": [
    {
      "id": "trip-wedding-lake-como",
      "packageType": "wedding",
      "title": "Lake Como wedding weekend",
      "destination": "Lake Como, Italy",
      "startDate": "2027-06-11",
      "endDate": "2027-06-13",
      "guestCount": 80,
      "biddingClosesAt": "2026-11-20T18:00:00.000Z",
      "revision": 2,
      "marketTotalUsd": 41200,
      "competition": "light",
      "alreadyBid": false,
      "matchesServices": true,
      "eventDetails": { "subtype": "wedding", "venueStatus": "booked", "venueLocation": { "city": "Como", "country": "Italy" }, "guestRange": { "min": 70, "max": 90 } },
      "items": [
        { "id": "item-catering", "category": "catering", "title": "Wedding dinner", "quantity": 1, "unit": "per_person", "perGuest": true, "referencePriceUsd": 9600 }
      ]
    }
  ],
  "page": { "limit": 20, "nextCursor": null, "hasMore": false, "total": 1 },
  "next": null
}
GET/api/v1/vendor/opportunities/{id}Get an opportunity
Bearer pfy_live_... · scope opportunities:read

One open opportunity with its items (unit-pricing fields for events), offerValidityHours, your ownBidId if you already bid, and offeringCandidates: the offerings of yours that fit each item, ready to link with offeringId when you bid. Closed, draft, own and unknown trips are 404.

Parameters & Fields
id
queryrequired
string (path)

The opportunity (trip) id.

Example Request (cURL)
curl -X GET "https://plannyfy.com/api/v1/vendor/opportunities/trip-wedding-lake-como" \
  -H "Authorization: Bearer pfy_live_YOUR_KEY"
Example Response200 OK
{
  "ok": true,
  "version": "v1",
  "opportunity": {
    "id": "trip-wedding-lake-como",
    "revision": 2,
    "guestCount": 80,
    "offerValidityHours": 72,
    "items": [{ "id": "item-catering", "category": "catering", "unit": "per_person", "perGuest": true }],
    "offeringCandidates": [
      { "itemId": "item-catering", "offeringId": "pf-como-menu", "title": "Lakeside tasting menu", "suggestedPriceUsd": 8800, "minGuests": 60 }
    ]
  }
}
GET/api/v1/vendor/bidsList your bids
Bearer pfy_live_... · scope bids:read

Your own bids. Live bids come most-at-risk first, past ones newest first. Each live bid carries a Bid Health band, never a rival's price or the traveler's budget. Lines include your verified offering link and its frozen snapshot.

Parameters & Fields
state
queryoptional
live | past | all (default live)

Which bids to list.

tripId
queryoptional
string

Only bids on this trip.

limit / cursor
queryoptional
integer 1-100 / string

Pagination.

Example Request (cURL)
curl -X GET "https://plannyfy.com/api/v1/vendor/bids?state=live" \
  -H "Authorization: Bearer pfy_live_YOUR_KEY"
Example Response200 OK
{
  "ok": true,
  "version": "v1",
  "bids": [
    {
      "id": "bid-1042",
      "tripId": "trip-wedding-lake-como",
      "state": "live",
      "status": "winning",
      "kind": "itemized",
      "total": 8800,
      "lines": [{ "itemId": "item-catering", "price": 8800, "unit": "per_person", "unitPrice": 110, "minGuests": 60, "inclusions": "Four courses, wine pairing", "offering": { "id": "pf-como-menu", "source": "manual" } }],
      "validUntil": "2026-11-23T18:00:00.000Z",
      "health": { "band": "strong", "position": "in_winning_mix", "hoursRemaining": 71.5, "expiringSoon": false }
    }
  ],
  "page": { "limit": 50, "nextCursor": null, "hasMore": false, "total": 1 },
  "next": null
}
POST/api/v1/vendor/bidsPlace a bid
Bearer pfy_live_... · scope bids:write

Places a live bid with exactly the composer's rules: the trip is open, not yours and unchanged since tripRevision; you have no other live bid on it; every line passes the price limits (not more than 50% above the market reference), unit pricing and minimum guests; every offeringId is verified as yours. Send an Idempotency-Key so a retry cannot place the bid twice. There is no draft for a bid that does not exist yet: save a draft of a live bid with PATCH and publish: false.

Parameters & Fields
Idempotency-Key
headeroptional
string (1-255)

Same key and body: the first answer is replayed for 24 hours. Same key, different body: 409.

tripId
bodyrequired
string

The opportunity id.

kind
bodyoptional
itemized | bundle

A bundle needs at least two lines.

lines[]
bodyrequired
{ itemId, price | unit + unitPrice (+ minGuests), inclusions?, offeringId? }

1-50 lines. Event items priced by unit: send unit and unitPrice and the server computes price.

floorPrice
bodyoptional
number

Your private minimum. Never shown to anyone else.

tripRevision
bodyoptional
integer

The opportunity revision you priced against; a mismatch is 409 trip_changed.

Example Request (cURL)
curl -X POST "https://plannyfy.com/api/v1/vendor/bids" \
  -H "Authorization: Bearer pfy_live_YOUR_KEY" \
  -H "Idempotency-Key: 7f1b8f0e-1d1c-4c4e-8a77-0a4f6a1f5c2e" \
  -H "Content-Type: application/json" \
  -d '{
    "tripId": "trip-wedding-lake-como",
    "tripRevision": 2,
    "lines": [
      {
        "itemId": "item-catering",
        "unit": "per_person",
        "unitPrice": 110,
        "minGuests": 60,
        "inclusions": "Four courses, wine pairing",
        "offeringId": "pf-como-menu"
      }
    ]
  }'
Example Response201 Created
{
  "ok": true,
  "version": "v1",
  "bid": { "id": "bid-1042", "tripId": "trip-wedding-lake-como", "state": "live", "kind": "itemized", "total": 8800, "validUntil": "2026-11-23T18:00:00.000Z" }
}
// 409 { "ok": false, "error": { "code": "bid_exists", "message": "You already have a live bid on this trip. ...", "requestId": "req_..." }, "bidId": "bid-1042" }
// 422 { "ok": false, "error": { "code": "validation_failed", "message": "\"Wedding dinner\" is more than 50% above market price. ...", "requestId": "req_..." } }
GET/api/v1/vendor/bids/{id}Get a bid
Bearer pfy_live_... · scope bids:read

One of your bids, live or past. Someone else's bid is the same 404 as a missing one.

Parameters & Fields
id
queryrequired
string (path)

The bid id.

Example Request (cURL)
curl -X GET "https://plannyfy.com/api/v1/vendor/bids/bid-1042" \
  -H "Authorization: Bearer pfy_live_YOUR_KEY"
Example Response200 OK
{ "ok": true, "version": "v1", "bid": { "id": "bid-1042", "state": "live", "status": "winning", "total": 8800 } }
PATCH/api/v1/vendor/bids/{id}Revise a bid or save a draft
Bearer pfy_live_... · scope bids:write

publish: true (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, and goes through when repeated with confirm: true. publish: false saves the lines as your private draft; nothing goes live. Lines you leave out are removed from the bid.

Parameters & Fields
lines
bodyrequired
array | null

The full set of lines (null with publish: false discards the draft).

publish
bodyoptional
boolean (default true)

false saves a private draft only.

confirm
bodyoptional
boolean

Confirms a price that is far below market or below your own floor.

tripRevision
bodyoptional
integer

A mismatch is 409 trip_changed.

Example Request (cURL)
curl -X PATCH "https://plannyfy.com/api/v1/vendor/bids/bid-1042" \
  -H "Authorization: Bearer pfy_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "publish": true,
    "lines": [
      {
        "itemId": "item-catering",
        "unit": "per_person",
        "unitPrice": 104,
        "minGuests": 60
      }
    ]
  }'
Example Response200 OK
{
  "ok": true,
  "version": "v1",
  "published": true,
  "stillPaused": false,
  "bid": { "id": "bid-1042", "total": 8320, "status": "winning" }
}
POST/api/v1/vendor/bids/{id}/withdrawWithdraw a live bid
Bearer pfy_live_... · scope bids:write

Withdraws a live bid before the customer locks the trip. Returns 409 trip_locked if the trip is already locked.

Parameters & Fields
id
queryrequired
string (path)

The bid id.

reason
bodyoptional
string (1-400)

Optional withdrawal reason.

Example Request (cURL)
curl -X POST "https://plannyfy.com/api/v1/vendor/bids/bid-1042/withdraw" \
  -H "Authorization: Bearer pfy_live_YOUR_KEY" \
  -H "Idempotency-Key: 7f1b8f0e-1d1c-4c4e-8a77-0a4f6a1f5c2e" \
  -H "Content-Type: application/json" \
  -d '{
    "reason": "Dates booked via direct channel"
  }'
Example Response200 OK
{ "ok": true, "version": "v1", "bidId": "bid-1042", "withdrawn": true, "reason": "Dates booked via direct channel" }
POST/api/v1/vendor/bids/{id}/confirmConfirm bid price after a customer trip edit
Bearer pfy_live_... · scope bids:write

Clears needsReprice on your live bid and aligns bidRevision with the trip's current revision after the customer edits dates, party size, or items.

Parameters & Fields
id
queryrequired
string (path)

The bid id.

Example Request (cURL)
curl -X POST "https://plannyfy.com/api/v1/vendor/bids/bid-1042/confirm" \
  -H "Authorization: Bearer pfy_live_YOUR_KEY" \
  -H "Idempotency-Key: 7f1b8f0e-1d1c-4c4e-8a77-0a4f6a1f5c2e"
Example Response200 OK
{ "ok": true, "version": "v1", "bidId": "bid-1042", "confirmed": true, "needsReprice": false, "bidRevision": 3 }
GET/api/v1/vendor/availabilityRead vendor availability rules & blackout dates
Bearer pfy_live_... · scope availability:read

Returns maxEventsPerDay, leadTimeDays, and blockedRanges for the authenticated vendor.

Parameters & Fields
Example Request (cURL)
curl -X GET "https://plannyfy.com/api/v1/vendor/availability" \
  -H "Authorization: Bearer pfy_live_YOUR_KEY"
Example Response200 OK
{
  "ok": true,
  "version": "v1",
  "vendorId": "vendor-atlas-dmc",
  "availability": {
    "maxEventsPerDay": 2,
    "leadTimeDays": 7,
    "blockedRanges": [{ "id": "blk-1", "startDate": "2027-08-10", "endDate": "2027-08-20", "reason": "Annual maintenance" }],
    "updatedAt": "2026-10-26T12:00:00.000Z"
  }
}
PUT/api/v1/vendor/availabilityUpdate vendor availability rules & blackout dates
Bearer pfy_live_... · scope availability:write

Updates maxEventsPerDay (1-10), leadTimeDays (0-365), and/or blockedRanges (up to 100 ranges in YYYY-MM-DD).

Parameters & Fields
maxEventsPerDay
bodyoptional
integer (1-10)

Maximum concurrent events or trips per day.

leadTimeDays
bodyoptional
integer (0-365)

Minimum days of notice required before startDate.

blockedRanges[]
bodyoptional
{ id?, startDate, endDate, reason? }

Blackout date ranges (YYYY-MM-DD).

Example Request (cURL)
curl -X PUT "https://plannyfy.com/api/v1/vendor/availability" \
  -H "Authorization: Bearer pfy_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "maxEventsPerDay": 2,
    "leadTimeDays": 7,
    "blockedRanges": [
      {
        "startDate": "2027-08-10",
        "endDate": "2027-08-20",
        "reason": "Summer closure"
      }
    ]
  }'
Example Response200 OK
{ "ok": true, "version": "v1", "vendorId": "vendor-atlas-dmc", "availability": { "maxEventsPerDay": 2, "leadTimeDays": 7, "blockedRanges": [{ "id": "blk-1", "startDate": "2027-08-10", "endDate": "2027-08-20", "reason": "Summer closure" }] } }
Group 6 · Earnings & Payouts

Earnings & Payouts API

Your won bookings with commission and net, the state of every payout, and your pipeline. Only your own awarded lines; never another vendor's data.

GET/api/v1/vendor/earningsList won bookings and earnings
Bearer pfy_live_... · scope earnings:read

Your won bookings (newest first) with awarded lines, commission, net and payout state, totals in summary, and your pipeline. Refunds and dispute outcomes lower the figures once the payout ledger records them.

Parameters & Fields
payout
queryoptional
pending | paid

Only bookings in this payout state.

limit / cursor
queryoptional
integer 1-100 / string

Pagination of the bookings.

Example Request (cURL)
curl -X GET "https://plannyfy.com/api/v1/vendor/earnings?payout=pending" \
  -H "Authorization: Bearer pfy_live_YOUR_KEY"
Example Response200 OK
{
  "ok": true,
  "version": "v1",
  "currency": "usd",
  "summary": { "totalGrossWon": 8800, "totalCommission": 968, "totalNetWon": 7832, "pendingNetAmount": 7832, "paidNetAmount": 0, "wonCount": 1 },
  "bookings": [
    { "bidId": "bid-1042", "grossAmount": 8800, "commissionAmount": 968, "netPayout": 7832, "fulfillmentStatus": "upcoming", "payoutStatus": "pending", "payout": { "status": "pending", "label": "Scheduled", "note": "" }, "payoutReleasesAt": "2027-06-13" }
  ],
  "pipeline": [],
  "page": { "limit": 50, "nextCursor": null, "hasMore": false, "total": 1 },
  "next": null
}
GET/api/v1/vendor/payoutsList payouts
Bearer pfy_live_... · scope payouts:read

The state of your payouts, newest first, in USD. Admin notes, hold reasons, raw payment-provider errors and account identifiers are never included: note is the sentence the app shows.

Parameters & Fields
status
queryoptional
pending | eligible | awaiting_approval | processing | paid | failed | held | reversed | cancelled

Only payouts in this state.

tripId
queryoptional
string

Only this trip.

limit / cursor
queryoptional
integer 1-100 / string

Pagination.

Example Request (cURL)
curl -X GET "https://plannyfy.com/api/v1/vendor/payouts?status=held" \
  -H "Authorization: Bearer pfy_live_YOUR_KEY"
Example Response200 OK
{
  "ok": true,
  "version": "v1",
  "currency": "usd",
  "payouts": [
    { "id": "po-8f2c", "tripId": "trip-wedding-lake-como", "status": "held", "label": "On hold", "note": "Finish payout setup in Billing and we'll release this.", "needsBilling": true, "grossUsd": 8800, "commissionUsd": 968, "netUsd": 7832, "eligibleAt": "2027-06-15T23:59:59.000Z" }
  ],
  "page": { "limit": 50, "nextCursor": null, "hasMore": false, "total": 1 },
  "next": null
}
Group 7 · Customer Trips, Lock, Escrow & Concierge

Customer Trips, Lock, Escrow & Concierge API

Create and publish customer trips or event packages, inspect the live #1 Best Price and #1 Best Value optimal mixes, lock winning vendor packages into escrow, confirm post-trip completion, open disputes, and exchange moderated concierge messages.

GET/api/v1/tripsList customer trips & event packages
Bearer pfy_live_... · scope trips:read

Lists the authenticated customer's trips (newest first) with live bid counts, winning-mix summaries, and cursor pagination.

Parameters & Fields
status
queryoptional
draft | open | locked | completed | disputed | cancelled

Filter by lifecycle status.

packageType
queryoptional
travel | wedding | corporate_event | ...

Filter by package type.

limit / cursor
queryoptional
integer 1-100 / string

Pagination.

Example Request (cURL)
curl -X GET "https://plannyfy.com/api/v1/trips?status=open&limit=20" \
  -H "Authorization: Bearer pfy_live_YOUR_KEY"
Example Response200 OK
{
  "ok": true,
  "version": "v1",
  "trips": [
    {
      "id": "trip-italy-anniversary",
      "title": "10-Day Amalfi & Rome Anniversary",
      "destination": "Amalfi Coast & Rome, Italy",
      "packageType": "travel",
      "status": "open",
      "priority": "balanced",
      "targetBudget": 8500,
      "retailEstimate": 9700,
      "bidCount": 4,
      "bestMixTotalUsd": 8290,
      "bestMixComplete": true
    }
  ],
  "page": { "limit": 20, "nextCursor": null, "hasMore": false, "total": 1 },
  "next": null
}
POST/api/v1/tripsCreate a customer trip or event package
Bearer pfy_live_... · scope trips:write

Creates a new trip or event package with 1-50 itinerary items. When draft is false (default), opens the reverse auction immediately and dispatches opportunity.matched webhooks to matching vendors.

Parameters & Fields
Idempotency-Key
headeroptional
string (1-255)

Makes retries safe for 24 hours.

destination
bodyrequired
string

City, region, or venue destination.

startDate / endDate
bodyrequired
string (YYYY-MM-DD)

Start and end dates.

targetBudget
bodyrequired
number

Sealed target budget in USD (never shown to vendors).

items[]
bodyrequired
{ category, title, description?, quantity?, referencePrice, mustHave?, unit?, perGuest? }

1-50 itinerary or event line items.

draft
bodyoptional
boolean (default false)

Save as private draft before opening bidding.

Example Request (cURL)
curl -X POST "https://plannyfy.com/api/v1/trips" \
  -H "Authorization: Bearer pfy_live_YOUR_KEY" \
  -H "Idempotency-Key: 7f1b8f0e-1d1c-4c4e-8a77-0a4f6a1f5c2e" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Amalfi Anniversary Escape",
    "destination": "Amalfi Coast, Italy",
    "startDate": "2027-05-10",
    "endDate": "2027-05-15",
    "partySize": 2,
    "targetBudget": 4800,
    "priority": "balanced",
    "items": [
      {
        "category": "hotel",
        "title": "5-Night Boutique Cliffside Hotel",
        "quantity": 1,
        "referencePrice": 3600
      },
      {
        "category": "activity",
        "title": "Private Capri Sunset Yacht Charter",
        "quantity": 1,
        "referencePrice": 1600
      }
    ]
  }'
Example Response201 Created
{ "ok": true, "version": "v1", "trip": { "id": "trip-custom-901", "status": "open", "destination": "Amalfi Coast, Italy", "targetBudget": 4800, "retailEstimate": 5200 } }
GET/api/v1/trips/{id}Get trip detail, live bids & optimal mix
Bearer pfy_live_... · scope trips:read

Returns the customer's trip with its items, the reverse-auction optimal mix (#1 Best Price and #1 Best Value picks), per-item offer rows, refunds, and disputes.

Parameters & Fields
id
queryrequired
string (path)

The trip id.

Example Request (cURL)
curl -X GET "https://plannyfy.com/api/v1/trips/trip-italy-anniversary" \
  -H "Authorization: Bearer pfy_live_YOUR_KEY"
Example Response200 OK
{
  "ok": true,
  "version": "v1",
  "trip": { "id": "trip-italy-anniversary", "status": "open", "priority": "balanced", "targetBudget": 8500, "retailEstimate": 9700 },
  "bidCount": 4,
  "exchange": {
    "picks": {
      "price": { "complete": true, "total": 8290, "savingsVsRetail": 1410, "savingsPct": 15, "budgetDelta": -210 },
      "value": { "complete": true, "total": 8820, "savingsVsRetail": 880, "savingsPct": 9, "budgetDelta": 320 }
    },
    "rows": []
  },
  "refunds": [],
  "disputes": []
}
PATCH/api/v1/trips/{id}Publish a draft, change priority, or edit an open trip
Bearer pfy_live_... · scope trips:write

Publishes a draft trip (publish: true), updates the ranking priority (price | balanced | quality), or edits trip dates, partySize, targetBudget, or items while open (incrementing revision and flagging live bids for vendor re-confirmation).

Parameters & Fields
id
queryrequired
string (path)

The trip id.

publish
bodyoptional
boolean

Set true to publish a draft trip and open bidding.

priority
bodyoptional
price | balanced | quality

Update the optimal-mix ranking priority.

destination / startDate / endDate / partySize / targetBudget / items
bodyoptional
object

Itinerary fields to update on a draft or open trip.

Example Request (cURL)
curl -X PATCH "https://plannyfy.com/api/v1/trips/trip-italy-anniversary" \
  -H "Authorization: Bearer pfy_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "priority": "quality"
  }'
Example Response200 OK
{ "ok": true, "version": "v1", "trip": { "id": "trip-italy-anniversary", "status": "open", "priority": "quality", "revision": 2 } }
POST/api/v1/trips/{id}/lockLock winning package mix into escrow
Bearer pfy_live_... · scope escrow:write

Locks the selected optimal mix (price or value pick) into Plannyfy escrow, awards winning vendor bids, creates vendor payout ledger entries (held until 48h after trip endDate), and dispatches trip.locked webhooks.

Parameters & Fields
id
queryrequired
string (path)

The trip id.

pick
bodyoptional
price | value (default value)

Which optimal mix to lock.

withInsurance
bodyoptional
boolean

Add Cancel-for-Any-Reason protection.

paymentMethodId
bodyoptional
string

Saved customer payment method id.

Example Request (cURL)
curl -X POST "https://plannyfy.com/api/v1/trips/trip-italy-anniversary/lock" \
  -H "Authorization: Bearer pfy_live_YOUR_KEY" \
  -H "Idempotency-Key: 7f1b8f0e-1d1c-4c4e-8a77-0a4f6a1f5c2e" \
  -H "Content-Type: application/json" \
  -d '{
    "pick": "value",
    "withInsurance": true,
    "paymentMethodId": "pm-visa-4242"
  }'
Example Response200 OK
{ "ok": true, "version": "v1", "tripId": "trip-italy-anniversary", "status": "locked", "lockedSelection": { "pick": "value", "total": 8820, "grandTotalUsd": 9173 }, "payoutEntriesCreated": 2 }
POST/api/v1/trips/{id}/completeRecord customer post-trip completion sign-off
Bearer pfy_live_... · scope escrow:write

Records the customer's 48-hour post-trip completion sign-off: confirmed marks the trip completed so escrow payouts release on schedule; issue_reported pauses payout release and opens a formal dispute.

Parameters & Fields
id
queryrequired
string (path)

The trip id.

status
bodyrequired
confirmed | issue_reported

Customer completion sign-off outcome.

note
bodyoptional
string (max 500)

Completion note or issue summary.

Example Request (cURL)
curl -X POST "https://plannyfy.com/api/v1/trips/trip-italy-anniversary/complete" \
  -H "Authorization: Bearer pfy_live_YOUR_KEY" \
  -H "Idempotency-Key: 7f1b8f0e-1d1c-4c4e-8a77-0a4f6a1f5c2e" \
  -H "Content-Type: application/json" \
  -d '{
    "status": "confirmed",
    "note": "Everything went smoothly across every vendor."
  }'
Example Response200 OK
{ "ok": true, "version": "v1", "tripId": "trip-italy-anniversary", "tripStatus": "completed", "completionCheck": { "status": "confirmed" } }
POST/api/v1/trips/{id}/cancelCancel a trip or delete a draft
Bearer pfy_live_... · scope escrow:write

Deletes a draft trip, cancels an open trip, or cancels a locked trip with automatic cancellation-policy refund calculation and payout ledger reversal.

Parameters & Fields
id
queryrequired
string (path)

The trip id.

reason
bodyoptional
string (max 400)

Cancellation reason.

Example Request (cURL)
curl -X POST "https://plannyfy.com/api/v1/trips/trip-italy-anniversary/cancel" \
  -H "Authorization: Bearer pfy_live_YOUR_KEY" \
  -H "Idempotency-Key: 7f1b8f0e-1d1c-4c4e-8a77-0a4f6a1f5c2e" \
  -H "Content-Type: application/json" \
  -d '{
    "reason": "Flight schedule changed"
  }'
Example Response200 OK
{ "ok": true, "version": "v1", "tripId": "trip-italy-anniversary", "cancelled": true, "refundAmountUsd": 8820 }
POST/api/v1/trips/{id}/disputesOpen an escrow dispute on a locked trip
Bearer pfy_live_... · scope escrow:write

Opens a formal escrow dispute on a locked trip, pauses vendor payout release until resolved by Plannyfy Trust & Safety, and dispatches dispute.updated webhooks.

Parameters & Fields
id
queryrequired
string (path)

The trip id.

reason
bodyrequired
string (3-200)

Short dispute reason.

details
bodyrequired
string (5-2000)

Detailed explanation for Trust & Safety review.

amountInDispute
bodyrequired
number

Amount in dispute in USD.

vendorId
bodyoptional
string

Specific vendor id (defaults to primary winning vendor).

Example Request (cURL)
curl -X POST "https://plannyfy.com/api/v1/trips/trip-italy-anniversary/disputes" \
  -H "Authorization: Bearer pfy_live_YOUR_KEY" \
  -H "Idempotency-Key: 7f1b8f0e-1d1c-4c4e-8a77-0a4f6a1f5c2e" \
  -H "Content-Type: application/json" \
  -d '{
    "reason": "Weather cancellation on yacht charter",
    "details": "Port authority closed Capri harbor; requesting charter line refund.",
    "amountInDispute": 1480
  }'
Example Response201 Created
{ "ok": true, "version": "v1", "dispute": { "id": "dsp-901", "tripId": "trip-italy-anniversary", "status": "open", "amountInDispute": 1480 } }
GET/api/v1/trips/{id}/messagesList moderated concierge messages on a trip
Bearer pfy_live_... · scope messages:read

Lists moderated messages on a trip thread. Accessible to the trip's customer or to an approved vendor with a bid on the trip.

Parameters & Fields
id
queryrequired
string (path)

The trip id.

vendorId
queryoptional
string

Optional vendor filter for customers.

Example Request (cURL)
curl -X GET "https://plannyfy.com/api/v1/trips/trip-italy-anniversary/messages" \
  -H "Authorization: Bearer pfy_live_YOUR_KEY"
Example Response200 OK
{
  "ok": true,
  "version": "v1",
  "tripId": "trip-italy-anniversary",
  "messages": [
    { "id": "msg-1", "tripId": "trip-italy-anniversary", "vendorId": "vendor-atlas-dmc", "senderRole": "customer", "senderName": "Elena & Marcus", "body": "Can you accommodate a gluten-free tasting menu?", "createdAt": "2026-10-26T14:00:00.000Z" }
  ]
}
POST/api/v1/trips/{id}/messagesSend a moderated concierge message on a trip
Bearer pfy_live_... · scope messages:write

Sends a message between the customer and a bidding/locked vendor. Blocks off-platform contact details (emails, phone numbers, URLs, social handles, payment links) with 422 message_blocked_contact_info.

Parameters & Fields
id
queryrequired
string (path)

The trip id.

body
bodyrequired
string (1-1500)

Message text (screened by the PII moderation guard).

vendorId
bodyoptional
string

Target vendor id (required when called by a customer).

Example Request (cURL)
curl -X POST "https://plannyfy.com/api/v1/trips/trip-italy-anniversary/messages" \
  -H "Authorization: Bearer pfy_live_YOUR_KEY" \
  -H "Idempotency-Key: 7f1b8f0e-1d1c-4c4e-8a77-0a4f6a1f5c2e" \
  -H "Content-Type: application/json" \
  -d '{
    "vendorId": "vendor-atlas-dmc",
    "body": "Can we schedule the transfer pickup for 10:30 AM?"
  }'
Example Response201 Created
{ "ok": true, "version": "v1", "message": { "id": "msg-2", "tripId": "trip-italy-anniversary", "vendorId": "vendor-atlas-dmc", "senderRole": "customer", "body": "Can we schedule the transfer pickup for 10:30 AM?" } }
Schema Reference

Plannyfy Common Schema v1 Fields & PMS Aliases

All supported fields on plannyfy.travel.portfolio.v1 and their accepted PMS/OTA aliases.

Canonical FieldAccepted PMS AliasesTypeDescription
externalIdsku, codestring (optional)Your internal PMS room/tour/charter code. Used for idempotent upserts across syncs.
categorytype"hotel" | "activity" | "guide" | "transfer" | "train" | "flight"Travel category. Auto-inferred from title/description if omitted.
titlename, offeringNamestring (required)Public title of the hotel suite, villa, yacht charter, guide, or transfer.
locationcity, destinationstringCity and country or region (e.g., 'Rome, Italy', 'Amalfi Coast, Italy').
pricingUnitunit"per_night" | "per_person" | "per_group" | "flat"How the base rate scales with trip nights or party size.
basePriceUsdprice, rate, netRate, amountUsdnumber (required)Starting bid rate in USD per pricing unit.
wholesaleRateUsd / subVendorMarkupPctmarkupPct, subVendorNamenumber / number (0–60)Sub-vendor net wholesale rate and primary vendor markup percentage (`effectiveBidUsd`).
seasonalRatespeakSummerUsd, shoulderUsd, lowSeasonUsd{ peakSummerUsd?: number; shoulderUsd?: number; lowSeasonUsd?: number }Peak (Jun–Aug), Shoulder (Apr–May, Sep–Oct), and Low (Nov–Mar) rates auto-matched to trip dates.
floorPctOfBasefloorPctnumber (60–100)Private minimum floor as % of base rate (never exposed to customers).
galleryUrlsphotos, imagesstring[] (HTTPS URLs)Multi-photo gallery URLs rendered in the customer offer showcase.
youtubeUrlvideoUrlstring (YouTube URL)4K walkthrough video embedded inline inside the customer's offer comparison.
videoTitle—stringCaption for your embedded video tour (e.g. '4K Suite & Pool Walkthrough').
socialReelUrlreelUrl, tiktokUrlstringOptional Instagram Reel or TikTok highlight URL.
highlightsfeaturesstring[]Structured experience/property highlights (e.g. 'Heated Infinity Pool').
includedPerksperks, inclusions, amenitiesstring[] or comma-separated stringIncluded extras (e.g., 'Daily breakfast included', 'Sea-view terrace upgrade').
tripAdvisorUrl / googleMapsUrl / brochurePdfUrl—string (HTTPS URL)Direct verification & digital fact-sheet links attached to your bids.
cancellationPolicycancellation"flexible_24h" | "moderate_7d" | "strict_14d" | "non_refundable"Pass-through cancellation window honored on locked bookings.