{
  "info": {
    "name": "Hızlıyo Vendor API v1 — Sandbox",
    "description": "Official Postman collection for the Hızlıyo Vendor API.\n\nMinimal set: one request per endpoint, in the order a real integration performs them. Together they cover the complete flow from credentials to a cancelled delivery.\n\nSource of truth: /openapi/vendor-v1.yaml. Where this collection and the specification disagree, the specification wins.\n\nSETUP\n1. Set the `clientId` and `clientSecret` collection variables. Both are issued when your integration application is approved.\n2. `baseUrl` defaults to sandbox: https://gw-dev.hizliyo.com/api/vendor/v1\n3. Run the requests top to bottom. Every request stores what the next one needs — token, fleet id, order id.\n\nORDER MATTERS. A store must exist before an order can be placed, and an order must exist before it can be queried or cancelled.\n\nDo not run this collection against production credentials (`hzl_live_`). Orders placed in production dispatch a real courier and are billed to the fleet.",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "variable": [
    {
      "key": "baseUrl",
      "value": "https://gw-dev.hizliyo.com/api/vendor/v1",
      "type": "string",
      "description": "Sandbox. Production: https://gw.hizliyo.com/api/vendor/v1"
    },
    {
      "key": "clientId",
      "value": "",
      "type": "string",
      "description": "Issued on approval"
    },
    {
      "key": "clientSecret",
      "value": "",
      "type": "string",
      "description": "Sandbox secrets are prefixed hzl_test_"
    },
    {
      "key": "accessToken",
      "value": "",
      "type": "string",
      "description": "Populated automatically by the token request"
    },
    {
      "key": "externalStoreId",
      "value": "TEST-001",
      "type": "string",
      "description": "Your own store identifier"
    },
    {
      "key": "fleetId",
      "value": "",
      "type": "string",
      "description": "Populated from availableFleets by the store discovery request"
    },
    {
      "key": "orderId",
      "value": "",
      "type": "string",
      "description": "Populated by the order creation request"
    }
  ],
  "auth": {
    "type": "bearer",
    "bearer": [
      {
        "key": "token",
        "value": "{{accessToken}}",
        "type": "string"
      }
    ]
  },
  "event": [
    {
      "listen": "prerequest",
      "script": {
        "type": "text/javascript",
        "exec": [
          "// Every order needs its own idempotency key. Reusing a key returns the original",
          "// response, so a fresh one is generated per run.",
          "pm.collectionVariables.set('runId', Date.now().toString(36).toUpperCase());"
        ]
      }
    }
  ],
  "item": [
    {
      "name": "/token — Issue access token",
      "event": [
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "const body = pm.response.json();",
              "",
              "pm.test('Status is 200', () => pm.response.to.have.status(200));",
              "pm.test('Access token returned', () => pm.expect(body.data.accessToken).to.be.a('string').and.not.empty);",
              "pm.test('Environment is sandbox', () => pm.expect(body.data.environment).to.eql('sandbox'));",
              "pm.test('Lifetime is 3600 seconds', () => pm.expect(body.data.expiresIn).to.eql(3600));",
              "",
              "pm.collectionVariables.set('accessToken', body.data.accessToken);",
              "",
              "// The vendor token must not carry a role claim. If it did, it could one day",
              "// satisfy a role check on a partner endpoint.",
              "const claims = JSON.parse(atob(body.data.accessToken.split('.')[1]));",
              "pm.test('Token carries no role claim', () => pm.expect(claims.role || claims.roles).to.be.undefined);",
              "console.log('iss:', claims.iss, '| aud:', claims.aud, '| env:', claims.env);"
            ]
          }
        }
      ],
      "request": {
        "auth": {
          "type": "noauth"
        },
        "method": "POST",
        "header": [
          {
            "key": "Content-Type",
            "value": "application/json"
          }
        ],
        "body": {
          "mode": "raw",
          "raw": "{\n  \"clientId\": \"{{clientId}}\",\n  \"clientSecret\": \"{{clientSecret}}\"\n}"
        },
        "url": {
          "raw": "{{baseUrl}}/token",
          "host": [
            "{{baseUrl}}"
          ],
          "path": [
            "token"
          ]
        },
        "description": "Exchanges credentials for a bearer token valid for one hour. Cache it; the endpoint is limited to 60 requests per hour."
      }
    },
    {
      "name": "/stores/{externalStoreId} — Register store and discover fleets",
      "event": [
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "const d = pm.response.json().data;",
              "",
              "pm.test('Status is 200', () => pm.response.to.have.status(200));",
              "pm.test('Store awaits fleet selection', () => pm.expect(d.status).to.eql('AwaitingFleet'));",
              "pm.test('Sub-region resolved', () => pm.expect(d.subRegion).to.be.a('string').and.not.empty);",
              "pm.test('Available fleets returned', () => pm.expect(d.availableFleets).to.be.an('array').that.is.not.empty);",
              "",
              "// Discovery exposes names and coverage area names only. Contact details and",
              "// polygons are withheld so the fleet cannot be approached directly.",
              "const f = d.availableFleets[0];",
              "pm.test('No contact details exposed', () => {",
              "    ['phone', 'email', 'taxNumber', 'contactName', 'polygon'].forEach(k =>",
              "        pm.expect(f[k], k + ' was exposed').to.be.undefined);",
              "});",
              "",
              "pm.collectionVariables.set('fleetId', f.fleetId);",
              "console.log('Selected fleet:', f.name, f.fleetId);"
            ]
          }
        }
      ],
      "request": {
        "method": "PUT",
        "header": [
          {
            "key": "Content-Type",
            "value": "application/json"
          }
        ],
        "body": {
          "mode": "raw",
          "raw": "{\n  \"taxNumber\": \"9876543210\",\n  \"name\": \"Test Restoran Balgat\",\n  \"phone\": \"5551112233\",\n  \"address\": {\n    \"text\": \"Oğuzlar Mahallesi 1370. Sokak No 12 Balgat Çankaya Ankara\",\n    \"latitude\": 39.899369564789325,\n    \"longitude\": 32.815135073097956\n  }\n}"
        },
        "url": {
          "raw": "{{baseUrl}}/stores/{{externalStoreId}}",
          "host": [
            "{{baseUrl}}"
          ],
          "path": [
            "stores",
            "{{externalStoreId}}"
          ]
        },
        "description": "Omit `fleetId` and the response carries `availableFleets` — there is no separate discovery endpoint.\n\nPATCH is blocked at the edge layer and never reaches the API. Always use PUT."
      }
    },
    {
      "name": "/stores/{externalStoreId} — Attach fleet",
      "event": [
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "const d = pm.response.json().data;",
              "",
              "pm.test('Status is 200', () => pm.response.to.have.status(200));",
              "pm.test('Fleet attached', () => pm.expect(d.fleet.fleetId).to.eql(pm.collectionVariables.get('fleetId')));",
              "pm.test('Discovery list no longer returned', () => pm.expect(d.availableFleets).to.be.undefined);",
              "console.log('Store status:', d.status);"
            ]
          }
        }
      ],
      "request": {
        "method": "PUT",
        "header": [
          {
            "key": "Content-Type",
            "value": "application/json"
          }
        ],
        "body": {
          "mode": "raw",
          "raw": "{\n  \"taxNumber\": \"9876543210\",\n  \"name\": \"Test Restoran Balgat\",\n  \"phone\": \"5551112233\",\n  \"address\": {\n    \"text\": \"Oğuzlar Mahallesi 1370. Sokak No 12 Balgat Çankaya Ankara\",\n    \"latitude\": 39.899369564789325,\n    \"longitude\": 32.815135073097956\n  },\n  \"fleetId\": \"{{fleetId}}\"\n}"
        },
        "url": {
          "raw": "{{baseUrl}}/stores/{{externalStoreId}}",
          "host": [
            "{{baseUrl}}"
          ],
          "path": [
            "stores",
            "{{externalStoreId}}"
          ]
        },
        "description": "The store becomes active only after the fleet confirms the connection — the fleet is the party that agreed the pricing."
      }
    },
    {
      "name": "/orders — Create delivery request",
      "event": [
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "const d = pm.response.json().data;",
              "",
              "pm.test('Status is 201', () => pm.response.to.have.status(201));",
              "pm.test('Order id returned', () => pm.expect(d.orderId).to.be.a('string').and.not.empty);",
              "pm.test('Initial status is Searching', () => pm.expect(d.status).to.eql('Searching'));",
              "pm.test('Tracking URL returned', () => pm.expect(d.trackingUrl).to.be.a('string'));",
              "",
              "pm.collectionVariables.set('orderId', d.orderId);",
              "console.log('Tracking:', d.trackingUrl);",
              "console.log('Sandbox timeline: +5s Assigned, +15s PickingUp, +30s PickedUp, +40s Delivering, +60s Delivered');"
            ]
          }
        }
      ],
      "request": {
        "method": "POST",
        "header": [
          {
            "key": "Content-Type",
            "value": "application/json"
          },
          {
            "key": "X-Idempotency-Key",
            "value": "PM-{{runId}}"
          }
        ],
        "body": {
          "mode": "raw",
          "raw": "{\n  \"externalStoreId\": \"{{externalStoreId}}\",\n  \"externalOrderId\": \"PM-ORD-{{runId}}\",\n  \"recipient\": {\n    \"name\": \"Ayşe Yılmaz\",\n    \"phone\": \"5339876543\"\n  },\n  \"dropoff\": {\n    \"text\": \"Ceyhun Atuf Kansu Caddesi No 45 Balgat Çankaya Ankara\",\n    \"latitude\": 39.902184,\n    \"longitude\": 32.820476,\n    \"buildingNo\": \"45\",\n    \"doorNo\": \"7\",\n    \"floor\": \"3\",\n    \"note\": \"Zili çalmayın, bebek uyuyor\"\n  },\n  \"packageNote\": \"1 adet döner, 1 ayran\",\n  \"orderAmount\": 285.50,\n  \"payment\": {\n    \"type\": \"Cash\",\n    \"collectionAmount\": 285.50\n  }\n}"
        },
        "url": {
          "raw": "{{baseUrl}}/orders",
          "host": [
            "{{baseUrl}}"
          ],
          "path": [
            "orders"
          ]
        },
        "description": "`orderAmount` is stored for reconciliation and reporting; it is not Hızlıyo revenue. `payment` is passed to the courier — Hızlıyo runs no collection workflow and there is no \"money received\" confirmation anywhere in the flow."
      }
    },
    {
      "name": "/orders/{orderId} — Status and event history",
      "event": [
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "const d = pm.response.json().data;",
              "",
              "pm.test('Status is 200', () => pm.response.to.have.status(200));",
              "pm.test('Event history returned', () => pm.expect(d.events).to.be.an('array'));",
              "console.log('Status:', d.status, '| events:', d.events.length);",
              "if (d.courier) console.log('Courier:', d.courier.name, d.courier.phone, d.courier.vehicleType);"
            ]
          }
        }
      ],
      "request": {
        "method": "GET",
        "header": [],
        "url": {
          "raw": "{{baseUrl}}/orders/{{orderId}}",
          "host": [
            "{{baseUrl}}"
          ],
          "path": [
            "orders",
            "{{orderId}}"
          ]
        },
        "description": "Limited to one request per order per ten seconds. Status is delivered by webhook; this endpoint is the fallback for when your endpoint was unreachable. There is no bulk or changed-since variant, and no separate /events endpoint — the history is part of this response."
      }
    },
    {
      "name": "/orders/{orderId}/cancel — Cancel before pickup",
      "event": [
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "if (pm.response.code === 200) {",
              "    pm.test('Order cancelled', () => pm.expect(pm.response.json().data.status).to.eql('Cancelled'));",
              "} else {",
              "    pm.test('Rejected with CANCEL_TOO_LATE', () => {",
              "        pm.expect(pm.response.code).to.eql(409);",
              "        pm.expect(pm.response.json().data.errorCode).to.eql('CANCEL_TOO_LATE');",
              "    });",
              "    console.log('The courier already collected the package — sandbox reaches PickedUp at +30s.');",
              "}"
            ]
          }
        }
      ],
      "request": {
        "method": "POST",
        "header": [
          {
            "key": "Content-Type",
            "value": "application/json"
          }
        ],
        "body": {
          "mode": "raw",
          "raw": "{\n  \"reason\": \"CustomerCancelled\"\n}"
        },
        "url": {
          "raw": "{{baseUrl}}/orders/{{orderId}}/cancel",
          "host": [
            "{{baseUrl}}"
          ],
          "path": [
            "orders",
            "{{orderId}}",
            "cancel"
          ]
        },
        "description": "Reason values: CustomerCancelled, RestaurantCancelled, AddressProblem, Other.\n\nRun this more than thirty seconds after creating a sandbox order and it returns 409 CANCEL_TOO_LATE — a path worth testing in its own right."
      }
    }
  ]
}
