{"info":{"name":"ZimmerStack Connect API","description":"The inbound half of the PMS integration: your PMS pushes availability, rates and restrictions to one property with one key; bookings come back through the outbound webhooks. Import this collection and its environment, set apiKey in the environment, run the folders top to bottom. Budgets, headers and errors: folder 4 and the contract's x-zimmerstack.rateLimits.","schema":"https://schema.getpostman.com/json/collection/v2.1.0/collection.json"},"auth":{"type":"bearer","bearer":[{"key":"token","value":"{{apiKey}}","type":"string"}]},"event":[{"listen":"prerequest","script":{"type":"text/javascript","exec":["// Dates a few weeks out, so the demo never touches tonight's inventory.","const d = (n) => { const t = new Date(); t.setUTCDate(t.getUTCDate() + n); return t.toISOString().slice(0, 10); };","if (!pm.collectionVariables.get(\"dateFrom\")) pm.collectionVariables.set(\"dateFrom\", d(30));","if (!pm.collectionVariables.get(\"dateTo\")) pm.collectionVariables.set(\"dateTo\", d(32));","// A fresh reservation id per run, so 5 · Create a booking creates rather than replays.","if (!pm.collectionVariables.get(\"externalReference\")) pm.collectionVariables.set(\"externalReference\", \"POSTMAN-\" + Date.now());"]}}],"variable":[{"key":"baseUrl","value":"https://api.zimmerstack.com","type":"string"},{"key":"apiKey","value":"","type":"string","description":"zsk_live_… — set it in the ENVIRONMENT, never here, so the collection can be shared"},{"key":"roomTypeId","value":"","type":"string","description":"set by 0 · Catalog"},{"key":"ratePlanId","value":"","type":"string","description":"set by 0 · Catalog"},{"key":"currency","value":"INR","type":"string","description":"set by 0 · Property"},{"key":"dateFrom","value":"","type":"string","description":"set by the collection pre-request script: today + 30"},{"key":"dateTo","value":"","type":"string","description":"set by the collection pre-request script: today + 32"},{"key":"batchId","value":"","type":"string","description":"set by the last write"},{"key":"idempotencyKey","value":"postman-replay-demo-0001","type":"string","description":"reused by 4 · Replay to show the same 202 comes back"},{"key":"bookingId","value":"","type":"string","description":"set by 5 · Create a booking"},{"key":"externalReference","value":"","type":"string","description":"set by the collection pre-request script: a fresh reservation id per run"}],"item":[{"name":"0 · Start here","description":"Two reads that tell you who the key is for and which numeric ids every write uses. Run them first; they fill the collection variables.","item":[{"name":"Property — who this key is for","request":{"method":"GET","header":[],"url":{"raw":"{{baseUrl}}/api/v1/connect-api/v1/property","host":["{{baseUrl}}"],"path":["api","v1","connect-api","v1","property"]},"description":"ariAuthoritative must be true for writes to reach the channels; limits are the budgets this key meets."},"event":[{"listen":"test","script":{"type":"text/javascript","exec":["pm.test(\"status 200\", () => pm.response.to.have.status(200));","const p = pm.response.json().data;","pm.collectionVariables.set(\"currency\", p.currency);","pm.test(\"the property is the source of truth\", () => pm.expect(p.ariAuthoritative).to.be.true);","pm.test(\"RateLimit headers present\", () => {","  pm.expect(pm.response.headers.has(\"RateLimit-Policy\")).to.be.true;","  pm.expect(pm.response.headers.has(\"RateLimit\")).to.be.true;","});"]}}]},{"name":"Catalog — room types and rate plans","request":{"method":"GET","header":[],"url":{"raw":"{{baseUrl}}/api/v1/connect-api/v1/catalog","host":["{{baseUrl}}"],"path":["api","v1","connect-api","v1","catalog"]},"description":"Stores the first room type and rate plan ids in the collection variables for the writes below."},"event":[{"listen":"test","script":{"type":"text/javascript","exec":["pm.test(\"status 200\", () => pm.response.to.have.status(200));","const c = pm.response.json().data;","if (c.roomTypes[0]) pm.collectionVariables.set(\"roomTypeId\", String(c.roomTypes[0].id));","if (c.ratePlans[0]) pm.collectionVariables.set(\"ratePlanId\", String(c.ratePlans[0].id));"]}}]}]},{"name":"1 · Read our record","description":"Reads come from our record, never from the channel manager, so they are cheap and never rate-limited by it.","item":[{"name":"Availability","request":{"method":"GET","header":[],"url":{"raw":"{{baseUrl}}/api/v1/connect-api/v1/availability?from={{dateFrom}}&to={{dateTo}}","host":["{{baseUrl}}"],"path":["api","v1","connect-api","v1","availability"],"query":[{"key":"from","value":"{{dateFrom}}","description":"YYYY-MM-DD in the property's timezone; default today"},{"key":"to","value":"{{dateTo}}","description":"YYYY-MM-DD; default from + 30 days, at most 500 days after from"}]}},"event":[{"listen":"test","script":{"type":"text/javascript","exec":["pm.test(\"status 200\", () => pm.response.to.have.status(200));"]}}]},{"name":"Rates","request":{"method":"GET","header":[],"url":{"raw":"{{baseUrl}}/api/v1/connect-api/v1/rates?from={{dateFrom}}&to={{dateTo}}","host":["{{baseUrl}}"],"path":["api","v1","connect-api","v1","rates"],"query":[{"key":"from","value":"{{dateFrom}}","description":"YYYY-MM-DD in the property's timezone; default today"},{"key":"to","value":"{{dateTo}}","description":"YYYY-MM-DD; default from + 30 days, at most 500 days after from"}]},"description":"rateMinor is in minor units (paise for INR)"},"event":[{"listen":"test","script":{"type":"text/javascript","exec":["pm.test(\"status 200\", () => pm.response.to.have.status(200));"]}}]},{"name":"Restrictions","request":{"method":"GET","header":[],"url":{"raw":"{{baseUrl}}/api/v1/connect-api/v1/restrictions?from={{dateFrom}}&to={{dateTo}}","host":["{{baseUrl}}"],"path":["api","v1","connect-api","v1","restrictions"],"query":[{"key":"from","value":"{{dateFrom}}","description":"YYYY-MM-DD in the property's timezone; default today"},{"key":"to","value":"{{dateTo}}","description":"YYYY-MM-DD; default from + 30 days, at most 500 days after from"}]}},"event":[{"listen":"test","script":{"type":"text/javascript","exec":["pm.test(\"status 200\", () => pm.response.to.have.status(200));"]}}]},{"name":"Sync — did everything land?","request":{"method":"GET","header":[],"url":{"raw":"{{baseUrl}}/api/v1/connect-api/v1/sync","host":["{{baseUrl}}"],"path":["api","v1","connect-api","v1","sync"]},"description":"inventory.state IN_SYNC and rates.dirtyCells 0 means nothing is waiting for the channels."},"event":[{"listen":"test","script":{"type":"text/javascript","exec":["pm.test(\"status 200\", () => pm.response.to.have.status(200));"]}}]}]},{"name":"2 · Write (202 + batch)","description":"Absolute values, ranges, last write wins. Each PUT answers 202 with a batchId and is pushed asynchronously. Every write here sends a fresh x-idempotency-key ({{$guid}}). Budgets: 30 writes a minute per key, 60 per property, 20000 cells a minute per property, 5000 cells and 1000 updates per request.","item":[{"name":"Availability — absolute count for a range","request":{"method":"PUT","header":[{"key":"Content-Type","value":"application/json","type":"text"},{"key":"x-idempotency-key","value":"{{$guid}}","type":"text"}],"url":{"raw":"{{baseUrl}}/api/v1/connect-api/v1/availability","host":["{{baseUrl}}"],"path":["api","v1","connect-api","v1","availability"]},"body":{"mode":"raw","raw":"{\n  \"updates\": [\n    {\n      \"roomTypeId\": {{roomTypeId}},\n      \"dateFrom\": \"{{dateFrom}}\",\n      \"dateTo\": \"{{dateTo}}\",\n      \"available\": 3\n    }\n  ]\n}","options":{"raw":{"language":"json"}}},"description":"An absolute count per night. A second identical send writes nothing and costs no channel-manager call."},"event":[{"listen":"test","script":{"type":"text/javascript","exec":["const body = pm.response.json();","if (body && body.data && body.data.batchId) pm.collectionVariables.set(\"batchId\", body.data.batchId);","pm.test(\"202 with a batchId\", () => { pm.response.to.have.status(202); pm.expect(body.data.batchId).to.be.a(\"string\"); });"]}}]},{"name":"Rates — major units with currency","request":{"method":"PUT","header":[{"key":"Content-Type","value":"application/json","type":"text"},{"key":"x-idempotency-key","value":"{{$guid}}","type":"text"}],"url":{"raw":"{{baseUrl}}/api/v1/connect-api/v1/rates","host":["{{baseUrl}}"],"path":["api","v1","connect-api","v1","rates"]},"body":{"mode":"raw","raw":"{\n  \"updates\": [\n    {\n      \"ratePlanId\": {{ratePlanId}},\n      \"dateFrom\": \"{{dateFrom}}\",\n      \"dateTo\": \"{{dateTo}}\",\n      \"rate\": \"4500.00\",\n      \"currency\": \"{{currency}}\"\n    }\n  ]\n}","options":{"raw":{"language":"json"}}},"description":"Sent in major units; read back in minor units (rateMinor)."},"event":[{"listen":"test","script":{"type":"text/javascript","exec":["const body = pm.response.json();","if (body && body.data && body.data.batchId) pm.collectionVariables.set(\"batchId\", body.data.batchId);","pm.test(\"202 with a batchId\", () => { pm.response.to.have.status(202); pm.expect(body.data.batchId).to.be.a(\"string\"); });"]}}]},{"name":"Restrictions — stay rules","request":{"method":"PUT","header":[{"key":"Content-Type","value":"application/json","type":"text"},{"key":"x-idempotency-key","value":"{{$guid}}","type":"text"}],"url":{"raw":"{{baseUrl}}/api/v1/connect-api/v1/restrictions","host":["{{baseUrl}}"],"path":["api","v1","connect-api","v1","restrictions"]},"body":{"mode":"raw","raw":"{\n  \"updates\": [\n    {\n      \"ratePlanId\": {{ratePlanId}},\n      \"dateFrom\": \"{{dateFrom}}\",\n      \"dateTo\": \"{{dateTo}}\",\n      \"minStayArrival\": 2,\n      \"closedToArrival\": false\n    }\n  ]\n}","options":{"raw":{"language":"json"}}},"description":"Only the fields you send change; the rest keep their values."},"event":[{"listen":"test","script":{"type":"text/javascript","exec":["const body = pm.response.json();","if (body && body.data && body.data.batchId) pm.collectionVariables.set(\"batchId\", body.data.batchId);","pm.test(\"202 with a batchId\", () => { pm.response.to.have.status(202); pm.expect(body.data.batchId).to.be.a(\"string\"); });"]}}]}]},{"name":"3 · Did it land?","description":"Poll no more than every 15 s. landed means the channel manager has it.","item":[{"name":"Batch status","request":{"method":"GET","header":[],"url":{"raw":"{{baseUrl}}/api/v1/connect-api/v1/batches/{{batchId}}","host":["{{baseUrl}}"],"path":["api","v1","connect-api","v1","batches","{{batchId}}"]}},"event":[{"listen":"test","script":{"type":"text/javascript","exec":["pm.test(\"status 200\", () => pm.response.to.have.status(200));","pm.test(\"status is pending or landed\", () => pm.expect([\"pending\", \"landed\"]).to.include(pm.response.json().data.status));"]}}]},{"name":"Sync","request":{"method":"GET","header":[],"url":{"raw":"{{baseUrl}}/api/v1/connect-api/v1/sync","host":["{{baseUrl}}"],"path":["api","v1","connect-api","v1","sync"]}},"event":[{"listen":"test","script":{"type":"text/javascript","exec":["pm.test(\"status 200\", () => pm.response.to.have.status(200));"]}}]}]},{"name":"4 · Budgets and errors","description":"What the API does when you go too fast or get something wrong. Budgets per minute: 120 requests and 30 writes per key; 300 requests and 60 writes per property; 240 requests per address; bodies up to 1048576 bytes. Every answer carries RateLimit-Policy and RateLimit; every 429 carries Retry-After and data.scope.","item":[{"name":"RateLimit headers on a normal answer","request":{"method":"GET","header":[],"url":{"raw":"{{baseUrl}}/api/v1/connect-api/v1/property","host":["{{baseUrl}}"],"path":["api","v1","connect-api","v1","property"]}},"event":[{"listen":"test","script":{"type":"text/javascript","exec":["pm.test(\"status 200\", () => pm.response.to.have.status(200));","pm.test(\"RateLimit headers present\", () => {","  pm.expect(pm.response.headers.has(\"RateLimit-Policy\")).to.be.true;","  pm.expect(pm.response.headers.has(\"RateLimit\")).to.be.true;","});"]}}]},{"name":"Replay — same idempotency key, same body, same 202","request":{"method":"PUT","header":[{"key":"Content-Type","value":"application/json","type":"text"},{"key":"x-idempotency-key","value":"{{idempotencyKey}}","type":"text"}],"url":{"raw":"{{baseUrl}}/api/v1/connect-api/v1/rates","host":["{{baseUrl}}"],"path":["api","v1","connect-api","v1","rates"]},"body":{"mode":"raw","raw":"{\n  \"updates\": [\n    {\n      \"ratePlanId\": {{ratePlanId}},\n      \"dateFrom\": \"{{dateFrom}}\",\n      \"dateTo\": \"{{dateTo}}\",\n      \"rate\": \"4500.00\",\n      \"currency\": \"{{currency}}\"\n    }\n  ]\n}","options":{"raw":{"language":"json"}}},"description":"Send it twice: the second answer is the stored 202 with X-Idempotent-Replay: true and the same batchId. Change the body with the same key and you get 409 IDEMPOTENCY_KEY_REUSED."},"event":[{"listen":"test","script":{"type":"text/javascript","exec":["pm.test(\"status 202\", () => pm.response.to.have.status(202));","pm.test(\"a batchId came back\", () => pm.expect(pm.response.json().data.batchId).to.be.a(\"string\"));"]}}]},{"name":"Bad key → 401 API_KEY_INVALID (no hint)","request":{"method":"GET","header":[],"url":{"raw":"{{baseUrl}}/api/v1/connect-api/v1/property","host":["{{baseUrl}}"],"path":["api","v1","connect-api","v1","property"]},"auth":{"type":"bearer","bearer":[{"key":"token","value":"zsk_live_0000000000000000000000000000000000000000000","type":"string"}]},"description":"The answer never says why. 20 of these from one address in a minute and the address is refused before any lookup (429, scope ip-auth)."},"event":[{"listen":"test","script":{"type":"text/javascript","exec":["pm.test(\"status 401\", () => pm.response.to.have.status(401));","pm.test(\"data.code API_KEY_INVALID\", () => pm.expect(pm.response.json().data.code).to.eql(\"API_KEY_INVALID\"));"]}}]},{"name":"Past the horizon → 400 HORIZON","request":{"method":"PUT","header":[{"key":"Content-Type","value":"application/json","type":"text"},{"key":"x-idempotency-key","value":"{{$guid}}","type":"text"}],"url":{"raw":"{{baseUrl}}/api/v1/connect-api/v1/availability","host":["{{baseUrl}}"],"path":["api","v1","connect-api","v1","availability"]},"body":{"mode":"raw","raw":"{\n  \"updates\": [\n    {\n      \"roomTypeId\": {{roomTypeId}},\n      \"dateFrom\": \"2099-01-01\",\n      \"dateTo\": \"2099-01-01\",\n      \"available\": 1\n    }\n  ]\n}","options":{"raw":{"language":"json"}}},"description":"The last writable night is today + 365 days."},"event":[{"listen":"test","script":{"type":"text/javascript","exec":["pm.test(\"status 400\", () => pm.response.to.have.status(400));","pm.test(\"data.code HORIZON\", () => pm.expect(pm.response.json().data.code).to.eql(\"HORIZON\"));"]}}]}]},{"name":"5 · Bookings (PMS side)","description":"For hotels that run the ZimmerStack PMS and their own booking engine (scopes bookings:write, bookings:read; bookings:payments to record money). One entry in rooms per room; you price the stay. The booking is written with its inventory event in one transaction and the channels update within seconds. Budgets: 20 bookings a minute per key, 40 per property, 500 per property per rolling day.","item":[{"name":"Create a booking (201; the same externalReference again → 200 replay)","request":{"method":"POST","header":[{"key":"Content-Type","value":"application/json","type":"text"},{"key":"x-idempotency-key","value":"{{$guid}}","type":"text"}],"url":{"raw":"{{baseUrl}}/api/v1/connect-api/v1/bookings","host":["{{baseUrl}}"],"path":["api","v1","connect-api","v1","bookings"]},"body":{"mode":"raw","raw":"{\n  \"externalReference\": \"{{externalReference}}\",\n  \"checkIn\": \"{{dateFrom}}\",\n  \"checkOut\": \"{{dateTo}}\",\n  \"currency\": \"{{currency}}\",\n  \"guest\": {\n    \"name\": \"Asha Verma\",\n    \"phone\": \"+919876543210\",\n    \"email\": \"asha@example.com\",\n    \"notes\": \"Late arrival, after 22:00\"\n  },\n  \"rooms\": [\n    {\n      \"roomTypeId\": {{roomTypeId}},\n      \"ratePlanId\": {{ratePlanId}},\n      \"adults\": 2,\n      \"children\": 1,\n      \"nightlyRate\": \"4500.00\"\n    },\n    {\n      \"roomTypeId\": {{roomTypeId}},\n      \"ratePlanId\": {{ratePlanId}},\n      \"adults\": 2,\n      \"nightlyRate\": \"4500.00\"\n    }\n  ],\n  \"payment\": {\n    \"amount\": \"9000.00\",\n    \"method\": \"ONLINE\",\n    \"reference\": \"pay_QwErTy123\"\n  },\n  \"notes\": \"Promo WINTER10 applied on the website\"\n}","options":{"raw":{"language":"json"}}},"description":"Needs bookings:write; the payment block needs bookings:payments (remove it otherwise). Dates are the collection's demo window, a few weeks out."},"event":[{"listen":"test","script":{"type":"text/javascript","exec":["const body = pm.response.json();","if (body && body.data && body.data.bookingId) pm.collectionVariables.set(\"bookingId\", String(body.data.bookingId));","pm.test(\"201 created or 200 replay, with a bookingId\", () => { pm.expect([200, 201]).to.include(pm.response.code); pm.expect(body.data.bookingId).to.be.a(\"number\"); });","pm.test(\"the inventory event was recorded with the booking\", () => pm.expect(body.data.inventory.eventRecorded).to.be.true);"]}}]},{"name":"The booking you just created","request":{"method":"GET","header":[],"url":{"raw":"{{baseUrl}}/api/v1/connect-api/v1/bookings/{{bookingId}}","host":["{{baseUrl}}"],"path":["api","v1","connect-api","v1","bookings","{{bookingId}}"]}},"event":[{"listen":"test","script":{"type":"text/javascript","exec":["pm.test(\"status 200\", () => pm.response.to.have.status(200));","pm.test(\"status UPCOMING\", () => pm.expect(pm.response.json().data.status).to.eql(\"UPCOMING\"));"]}}]},{"name":"Bookings the API created (last 7 days)","request":{"method":"GET","header":[],"url":{"raw":"{{baseUrl}}/api/v1/connect-api/v1/bookings?limit=20","host":["{{baseUrl}}"],"path":["api","v1","connect-api","v1","bookings"],"query":[{"key":"limit","value":"20"}]}},"event":[{"listen":"test","script":{"type":"text/javascript","exec":["pm.test(\"status 200\", () => pm.response.to.have.status(200));"]}}]},{"name":"Cancel it (idempotent; the nights re-open on the channels)","request":{"method":"POST","header":[{"key":"Content-Type","value":"application/json","type":"text"},{"key":"x-idempotency-key","value":"{{$guid}}","type":"text"}],"url":{"raw":"{{baseUrl}}/api/v1/connect-api/v1/bookings/{{bookingId}}/cancel","host":["{{baseUrl}}"],"path":["api","v1","connect-api","v1","bookings","{{bookingId}}","cancel"]},"body":{"mode":"raw","raw":"{\n  \"reason\": \"Guest cancelled on the website\"\n}","options":{"raw":{"language":"json"}}}},"event":[{"listen":"test","script":{"type":"text/javascript","exec":["pm.test(\"status 200\", () => pm.response.to.have.status(200));","pm.test(\"status CANCELLED\", () => pm.expect(pm.response.json().data.status).to.eql(\"CANCELLED\"));"]}}]}]},{"name":"Contract (OpenAPI 3.1, no auth)","item":[{"name":"GET the contract","request":{"method":"GET","header":[],"url":{"raw":"{{baseUrl}}/api/v1/public/connect-api/contract","host":["{{baseUrl}}"],"path":["api","v1","public","connect-api","contract"]},"auth":{"type":"noauth"},"description":"Every endpoint, schema, sample and error code; x-zimmerstack.rateLimits is the budget table."},"event":[{"listen":"test","script":{"type":"text/javascript","exec":["pm.test(\"status 200\", () => pm.response.to.have.status(200));","pm.test(\"OpenAPI 3.1\", () => pm.expect(pm.response.json().openapi).to.eql(\"3.1.0\"));"]}}]}]}]}