{
 "info": {
  "_postman_id": "b7e3f1a0-1111-4c00-9d00-portchief001",
  "name": "PortChief API — Developer Collection",
  "description": "Server-to-server REST API to publish, search, update and withdraw container listings (offers & needs) on PortChief.\n\n**Collection version: v1.1.1** — matches the API contract (OpenAPI `info.version`).\n\n**Quick start (2 minutes):**\n1. Open the collection **Variables** tab and fill in `username` and `password` (your PortChief account credentials).\n2. Run **1 · Authenticate → Get a token**. The token is saved automatically — every other request inherits it.\n3. Run **2 · Create a listing → Create an offer — minimal**. The listing `id` is saved automatically into `{{listing_id}}` and reused by Read / Update / Withdraw.\n\n**The listing lifecycle — one `id` rules everything:**\n```\nPOST /listings ──► 201 { id, url }     ⚠️ STORE THIS id (lost it? GET /listings?mine=1)\n        │\n        ├──► GET    /listings/{id}     read it\n        ├──► PATCH  /listings/{id}     update it\n        └──► DELETE /listings/{id}     withdraw it → \"withdrawn\", then 404 for everyone\n```\n\n**5 golden rules:**\n1. **Store the `id` returned by the 201** — it is your handle on a listing. Lost it? `GET /listings?mine=1` returns every listing you own (see *3 · Read & search → My listings*).\n2. `price` is allowed on **offers only** — a need with a price is rejected.\n3. `city` must be an existing city slug: `abidjan`, `cotonou`, `dunkerque`, `los-angeles`, `marseille-fos`, `montreal`, `vancouver`. The 400 validation error always returns the authoritative, up-to-date list.\n4. Validation errors are **self-fixing**: read `data.fields` in the 400 body — it names the offending field and the rule to satisfy.\n5. Unknown fields and query params are **silently ignored** — a typo never errors, it just does nothing.\n\n**Good to know:**\n- Base URL: `https://portchief.com/wp-json/portchief/v1`\n- Auth: JWT Bearer token, valid 30 days. Your identity and organization come from the token — never from the request body.\n- Every request has **saved examples** (success + errors): open the request's Examples dropdown to see what responses look like without sending anything.\n\nFull reference: https://portchief.com/developers/",
  "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
 },
 "auth": {
  "type": "bearer",
  "bearer": [
   {
    "key": "token",
    "value": "{{jwt_token}}",
    "type": "string"
   }
  ]
 },
 "event": [
  {
   "listen": "prerequest",
   "script": {
    "type": "text/javascript",
    "exec": [
     "// Auto-compute a realistic pickup window (today+7 → today+14) so examples never go stale.",
     "const iso = d => d.toISOString().slice(0, 10);",
     "const now = Date.now();",
     "pm.collectionVariables.set('pw_start', iso(new Date(now + 7 * 864e5)));",
     "pm.collectionVariables.set('pw_end', iso(new Date(now + 14 * 864e5)));"
    ]
   }
  }
 ],
 "variable": [
  {
   "key": "base_url",
   "value": "https://portchief.com/wp-json/portchief/v1",
   "type": "string"
  },
  {
   "key": "username",
   "value": "",
   "type": "string",
   "description": "Your PortChief account email"
  },
  {
   "key": "password",
   "value": "",
   "type": "string",
   "description": "Your PortChief account password"
  },
  {
   "key": "jwt_token",
   "value": "",
   "type": "string",
   "description": "Filled automatically by 'Get a token'"
  },
  {
   "key": "listing_id",
   "value": "",
   "type": "string",
   "description": "Filled automatically when you create a listing"
  },
  {
   "key": "pw_start",
   "value": "",
   "type": "string",
   "description": "Auto-computed pickup window start (today + 7 days)"
  },
  {
   "key": "pw_end",
   "value": "",
   "type": "string",
   "description": "Auto-computed pickup window end (today + 14 days)"
  }
 ],
 "item": [
  {
   "name": "1 · Authenticate",
   "description": "Exchange your credentials for a JWT token (valid 30 days). The token is saved automatically to `{{jwt_token}}` and inherited by every other request in this collection — you only run this once per session.",
   "item": [
    {
     "name": "Get a token",
     "event": [
      {
       "listen": "test",
       "script": {
        "type": "text/javascript",
        "exec": [
         "pm.test('Token received (200)', () => pm.response.to.have.status(200));",
         "const json = pm.response.json();",
         "if (json.token) {",
         "    pm.collectionVariables.set('jwt_token', json.token);",
         "    console.log('Token saved — all requests now authenticate automatically.');",
         "}"
        ]
       }
      }
     ],
     "request": {
      "auth": {
       "type": "noauth"
      },
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"username\": \"{{username}}\",\n  \"password\": \"{{password}}\"\n}"
      },
      "url": {
       "raw": "{{base_url}}/auth/token",
       "host": [
        "{{base_url}}"
       ],
       "path": [
        "auth",
        "token"
       ]
      },
      "description": "The only endpoint that does not require a token. Send your PortChief credentials, receive a JWT valid for 30 days (`expires_in` is in seconds).\n\nAll other endpoints expect the header `Authorization: Bearer <token>` — this collection adds it for you automatically."
     },
     "response": [
      {
       "name": "200 — token issued",
       "originalRequest": {
        "method": "POST",
        "header": [
         {
          "key": "Content-Type",
          "value": "application/json"
         }
        ],
        "body": {
         "mode": "raw",
         "raw": "{\n  \"username\": \"you@example.com\",\n  \"password\": \"••••••••\"\n}"
        },
        "url": {
         "raw": "{{base_url}}/auth/token",
         "host": [
          "{{base_url}}"
         ],
         "path": [
          "auth",
          "token"
         ]
        }
       },
       "status": "OK",
       "code": 200,
       "_postman_previewlanguage": "json",
       "header": [
        {
         "key": "Content-Type",
         "value": "application/json; charset=UTF-8"
        }
       ],
       "cookie": [],
       "body": "{\n  \"token\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3BvcnRjaGllZi5jb20iLCJpYXQiOjE3NTI2NTI4MDAsImV4cCI6MTc1NTI0NDgwMCwidWlkIjoyMX0.4x8mQ2sig\",\n  \"token_type\": \"Bearer\",\n  \"expires_in\": 2592000,\n  \"user\": {\n    \"id\": 21,\n    \"email\": \"you@example.com\",\n    \"name\": \"Your Company\"\n  }\n}"
      },
      {
       "name": "401 — invalid credentials",
       "originalRequest": {
        "method": "POST",
        "header": [
         {
          "key": "Content-Type",
          "value": "application/json"
         }
        ],
        "body": {
         "mode": "raw",
         "raw": "{\n  \"username\": \"you@example.com\",\n  \"password\": \"wrong-password\"\n}"
        },
        "url": {
         "raw": "{{base_url}}/auth/token",
         "host": [
          "{{base_url}}"
         ],
         "path": [
          "auth",
          "token"
         ]
        }
       },
       "status": "Unauthorized",
       "code": 401,
       "_postman_previewlanguage": "json",
       "header": [
        {
         "key": "Content-Type",
         "value": "application/json; charset=UTF-8"
        }
       ],
       "cookie": [],
       "body": "{\n  \"code\": \"pc_auth_failed\",\n  \"message\": \"Invalid username or password.\",\n  \"data\": {\n    \"status\": 401\n  }\n}"
      }
     ]
    }
   ]
  },
  {
   "name": "2 · Create a listing",
   "description": "Publish a container listing. Two directions exist:\n- **offer** — you HAVE containers available (a price ≥ 0 is allowed)\n- **need** — you are LOOKING FOR containers (no price allowed)\n\nRequired fields: `direction`, `container_type`, `city`, and a location (`lat` + `lng`, or an `address` we geocode for you).\n\n**The 201 response returns the listing `id` — store it.** It is your handle for updating and withdrawing the listing later. This collection saves it into `{{listing_id}}` automatically.",
   "item": [
    {
     "name": "Create an offer — minimal (4 required fields)",
     "event": [
      {
       "listen": "test",
       "script": {
        "type": "text/javascript",
        "exec": [
         "pm.test('Listing created (201)', () => pm.response.to.have.status(201));",
         "const json = pm.response.json();",
         "if (json.id) {",
         "    pm.collectionVariables.set('listing_id', json.id);",
         "    console.log('Listing id saved: ' + json.id + ' — Read / Update / Withdraw now target it.');",
         "}"
        ]
       }
      }
     ],
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"direction\": \"offer\",\n  \"container_type\": \"40HC\",\n  \"city\": \"marseille-fos\",\n  \"lat\": 43.3,\n  \"lng\": 5.37\n}"
      },
      "url": {
       "raw": "{{base_url}}/listings",
       "host": [
        "{{base_url}}"
       ],
       "path": [
        "listings"
       ]
      },
      "description": "The **bare minimum** to be live on the marketplace — 4 pieces of information:\n\n1. `direction` — `offer` or `need`\n2. `container_type` — e.g. `40HC` (send an unknown value and the 400 error lists all valid types)\n3. `city` — one of the 6 city slugs\n4. a location — `lat` + `lng` (or an `address` we geocode for you)\n\nEverything else is filled in for you:\n- **title** → auto-generated (e.g. \"Offer: 40HC · Marseille-Fos\")\n- **price** → 0 (free)\n- **pickup window** → starts today, open-ended\n- **contact email/phone** → taken from your organization profile\n\nSend it, get your `201 { id, url }`, open the `url` in a browser — your listing is live. Then see \"Create an offer — full options\" for every lever you can add."
     },
     "response": [
      {
       "name": "201 — created (title auto-generated)",
       "originalRequest": {
        "method": "POST",
        "header": [
         {
          "key": "Content-Type",
          "value": "application/json"
         }
        ],
        "body": {
         "mode": "raw",
         "raw": "{\n  \"direction\": \"offer\",\n  \"container_type\": \"40HC\",\n  \"city\": \"marseille-fos\",\n  \"lat\": 43.3,\n  \"lng\": 5.37\n}"
        },
        "url": {
         "raw": "{{base_url}}/listings",
         "host": [
          "{{base_url}}"
         ],
         "path": [
          "listings"
         ]
        }
       },
       "status": "Created",
       "code": 201,
       "_postman_previewlanguage": "json",
       "header": [
        {
         "key": "Content-Type",
         "value": "application/json; charset=UTF-8"
        }
       ],
       "cookie": [],
       "body": "{\n  \"id\": 10125,\n  \"url\": \"https://portchief.com/listing/offer-40hc-marseille-fos/\",\n  \"status\": \"publish\"\n}"
      },
      {
       "name": "201 — with address instead of lat/lng",
       "originalRequest": {
        "method": "POST",
        "header": [
         {
          "key": "Content-Type",
          "value": "application/json"
         }
        ],
        "body": {
         "mode": "raw",
         "raw": "{\n  \"direction\": \"offer\",\n  \"container_type\": \"40HC\",\n  \"city\": \"marseille-fos\",\n  \"address\": \"Fos-sur-Mer, France\"\n}"
        },
        "url": {
         "raw": "{{base_url}}/listings",
         "host": [
          "{{base_url}}"
         ],
         "path": [
          "listings"
         ]
        }
       },
       "status": "Created",
       "code": 201,
       "_postman_previewlanguage": "json",
       "header": [
        {
         "key": "Content-Type",
         "value": "application/json; charset=UTF-8"
        }
       ],
       "cookie": [],
       "body": "{\n  \"id\": 10126,\n  \"url\": \"https://portchief.com/listing/offer-40hc-marseille-fos-2/\",\n  \"status\": \"publish\"\n}"
      },
      {
       "name": "422 — address could not be located",
       "originalRequest": {
        "method": "POST",
        "header": [
         {
          "key": "Content-Type",
          "value": "application/json"
         }
        ],
        "body": {
         "mode": "raw",
         "raw": "{\n  \"direction\": \"offer\",\n  \"container_type\": \"40HC\",\n  \"city\": \"marseille-fos\",\n  \"address\": \"zzzzqqq nowhere land\"\n}"
        },
        "url": {
         "raw": "{{base_url}}/listings",
         "host": [
          "{{base_url}}"
         ],
         "path": [
          "listings"
         ]
        }
       },
       "status": "Unprocessable Entity",
       "code": 422,
       "_postman_previewlanguage": "json",
       "header": [
        {
         "key": "Content-Type",
         "value": "application/json; charset=UTF-8"
        }
       ],
       "cookie": [],
       "body": "{\n  \"code\": \"pc_geocode_failed\",\n  \"message\": \"Could not locate address: 'zzzzqqq nowhere land'. Provide lat/lng or refine the address.\",\n  \"data\": {\n    \"status\": 422\n  }\n}"
      }
     ]
    },
    {
     "name": "Create an offer — full options",
     "event": [
      {
       "listen": "test",
       "script": {
        "type": "text/javascript",
        "exec": [
         "pm.test('Listing created (201)', () => pm.response.to.have.status(201));",
         "const json = pm.response.json();",
         "if (json.id) {",
         "    pm.collectionVariables.set('listing_id', json.id);",
         "    console.log('Listing id saved: ' + json.id + ' — Read / Update / Withdraw now target it.');",
         "}"
        ]
       }
      }
     ],
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"direction\": \"offer\",\n  \"container_type\": \"40HC\",\n  \"city\": \"marseille-fos\",\n  \"lat\": 43.3,\n  \"lng\": 5.37,\n  \"title\": \"40HC available — Marseille-Fos terminal\",\n  \"description\": \"Five 40HC containers available for pickup at the Marseille-Fos terminal.\",\n  \"price\": 350,\n  \"pickup_window\": {\n    \"start\": \"{{pw_start}}\",\n    \"end\": \"{{pw_end}}\"\n  }\n}"
      },
      "url": {
       "raw": "{{base_url}}/listings",
       "host": [
        "{{base_url}}"
       ],
       "path": [
        "listings"
       ]
      },
      "description": "Publish an offer using **every optional lever**. For the bare minimum (4 required fields), see \"Create an offer — minimal\".\n\nWhat each option changes on the published listing:\n\n| Optional field | Visible effect |\n|---|---|\n| `title` | your own title instead of the auto-generated one |\n| `description` | the listing body text (max 5000 chars) |\n| `price` | the price badge — offers only, site currency, defaults to 0 |\n| `pickup_window` | the \"Available Dates\" calendar — `{ \"start\", \"end\" }`, `YYYY-MM-DD`, start ≤ end |\n| `image_base64` | hero photo + thumbnail in search results (base64 JPEG/PNG/WebP) |\n| `address` instead of `lat`/`lng` | we geocode it for you (422 if it cannot be located) |\n\nYour contact details (email/phone) come from your organization profile automatically — they are never sent in the payload."
     },
     "response": [
      {
       "name": "201 — listing created",
       "originalRequest": {
        "method": "POST",
        "header": [
         {
          "key": "Content-Type",
          "value": "application/json"
         }
        ],
        "body": {
         "mode": "raw",
         "raw": "{\n  \"direction\": \"offer\",\n  \"container_type\": \"40HC\",\n  \"city\": \"marseille-fos\",\n  \"lat\": 43.3,\n  \"lng\": 5.37,\n  \"price\": 350\n}"
        },
        "url": {
         "raw": "{{base_url}}/listings",
         "host": [
          "{{base_url}}"
         ],
         "path": [
          "listings"
         ]
        }
       },
       "status": "Created",
       "code": 201,
       "_postman_previewlanguage": "json",
       "header": [
        {
         "key": "Content-Type",
         "value": "application/json; charset=UTF-8"
        }
       ],
       "cookie": [],
       "body": "{\n  \"id\": 10123,\n  \"url\": \"https://portchief.com/listing/offer-40hc-marseille-fos/\",\n  \"status\": \"publish\"\n}"
      },
      {
       "name": "400 — validation error (unknown city)",
       "originalRequest": {
        "method": "POST",
        "header": [
         {
          "key": "Content-Type",
          "value": "application/json"
         }
        ],
        "body": {
         "mode": "raw",
         "raw": "{\n  \"direction\": \"offer\",\n  \"container_type\": \"40HC\",\n  \"city\": \"paris\",\n  \"lat\": 48.85,\n  \"lng\": 2.35\n}"
        },
        "url": {
         "raw": "{{base_url}}/listings",
         "host": [
          "{{base_url}}"
         ],
         "path": [
          "listings"
         ]
        }
       },
       "status": "Bad Request",
       "code": 400,
       "_postman_previewlanguage": "json",
       "header": [
        {
         "key": "Content-Type",
         "value": "application/json; charset=UTF-8"
        }
       ],
       "cookie": [],
       "body": "{\n  \"code\": \"pc_validation\",\n  \"message\": \"Invalid listing payload.\",\n  \"data\": {\n    \"status\": 400,\n    \"fields\": {\n      \"city\": \"Unknown city: 'paris'. Valid: abidjan, cotonou, dunkerque, los-angeles, marseille-fos, montreal, vancouver.\"\n    }\n  }\n}"
      },
      {
       "name": "401 — missing or invalid token",
       "originalRequest": {
        "method": "POST",
        "header": [
         {
          "key": "Content-Type",
          "value": "application/json"
         }
        ],
        "body": {
         "mode": "raw",
         "raw": "{\n  \"direction\": \"offer\",\n  \"container_type\": \"40HC\",\n  \"city\": \"marseille-fos\",\n  \"lat\": 43.3,\n  \"lng\": 5.37\n}"
        },
        "url": {
         "raw": "{{base_url}}/listings",
         "host": [
          "{{base_url}}"
         ],
         "path": [
          "listings"
         ]
        }
       },
       "status": "Unauthorized",
       "code": 401,
       "_postman_previewlanguage": "json",
       "header": [
        {
         "key": "Content-Type",
         "value": "application/json; charset=UTF-8"
        }
       ],
       "cookie": [],
       "body": "{\n  \"code\": \"rest_forbidden\",\n  \"message\": \"Sorry, you are not allowed to do that.\",\n  \"data\": {\n    \"status\": 401\n  }\n}"
      }
     ]
    },
    {
     "name": "Create a need",
     "event": [
      {
       "listen": "test",
       "script": {
        "type": "text/javascript",
        "exec": [
         "pm.test('Listing created (201)', () => pm.response.to.have.status(201));",
         "const json = pm.response.json();",
         "if (json.id) {",
         "    pm.collectionVariables.set('listing_id', json.id);",
         "    console.log('Listing id saved: ' + json.id);",
         "}"
        ]
       }
      }
     ],
     "request": {
      "method": "POST",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"direction\": \"need\",\n  \"container_type\": \"Reefer\",\n  \"city\": \"cotonou\",\n  \"lat\": 6.35,\n  \"lng\": 2.43,\n  \"description\": \"Looking for two reefer containers in Cotonou for perishable cargo.\"\n}"
      },
      "url": {
       "raw": "{{base_url}}/listings",
       "host": [
        "{{base_url}}"
       ],
       "path": [
        "listings"
       ]
      },
      "description": "Publish a **need** (containers you are looking for). Same fields as an offer, with one rule: **`price` is not allowed on needs** — sending one returns a 400 (see the saved example).\n\nThe title is auto-generated when omitted (e.g. \"Need: Reefer · Cotonou\")."
     },
     "response": [
      {
       "name": "201 — need created (auto-generated title)",
       "originalRequest": {
        "method": "POST",
        "header": [
         {
          "key": "Content-Type",
          "value": "application/json"
         }
        ],
        "body": {
         "mode": "raw",
         "raw": "{\n  \"direction\": \"need\",\n  \"container_type\": \"Reefer\",\n  \"city\": \"cotonou\",\n  \"lat\": 6.35,\n  \"lng\": 2.43\n}"
        },
        "url": {
         "raw": "{{base_url}}/listings",
         "host": [
          "{{base_url}}"
         ],
         "path": [
          "listings"
         ]
        }
       },
       "status": "Created",
       "code": 201,
       "_postman_previewlanguage": "json",
       "header": [
        {
         "key": "Content-Type",
         "value": "application/json; charset=UTF-8"
        }
       ],
       "cookie": [],
       "body": "{\n  \"id\": 10124,\n  \"url\": \"https://portchief.com/listing/need-reefer-cotonou/\",\n  \"status\": \"publish\"\n}"
      },
      {
       "name": "400 — price is not allowed on a need",
       "originalRequest": {
        "method": "POST",
        "header": [
         {
          "key": "Content-Type",
          "value": "application/json"
         }
        ],
        "body": {
         "mode": "raw",
         "raw": "{\n  \"direction\": \"need\",\n  \"container_type\": \"Reefer\",\n  \"city\": \"cotonou\",\n  \"lat\": 6.35,\n  \"lng\": 2.43,\n  \"price\": 100\n}"
        },
        "url": {
         "raw": "{{base_url}}/listings",
         "host": [
          "{{base_url}}"
         ],
         "path": [
          "listings"
         ]
        }
       },
       "status": "Bad Request",
       "code": 400,
       "_postman_previewlanguage": "json",
       "header": [
        {
         "key": "Content-Type",
         "value": "application/json; charset=UTF-8"
        }
       ],
       "cookie": [],
       "body": "{\n  \"code\": \"pc_validation\",\n  \"message\": \"Invalid listing payload.\",\n  \"data\": {\n    \"status\": 400,\n    \"fields\": {\n      \"price\": \"price is only allowed on offers (direction=offer).\"\n    }\n  }\n}"
      }
     ]
    }
   ]
  },
  {
   "name": "3 · Read & search",
   "description": "Search the marketplace or read a single listing. All read endpoints require a token (market data is reserved for authenticated members).\n\nFilters are combinable: `direction`, `city`, `container_type`, `pickup_from` / `pickup_to` (YYYY-MM-DD), `mine=1` (only your own listings), plus geographic search (`lat` + `lng` + `radius_km` — each result then carries `distance_km`). Results are paginated (`page`, `per_page` up to 100).",
   "item": [
    {
     "name": "Search by city",
     "event": [
      {
       "listen": "test",
       "script": {
        "type": "text/javascript",
        "exec": [
         "pm.test('Search OK (200)', () => pm.response.to.have.status(200));"
        ]
       }
      }
     ],
     "request": {
      "method": "GET",
      "header": [],
      "url": {
       "raw": "{{base_url}}/listings?city=marseille-fos&direction=offer&per_page=20",
       "host": [
        "{{base_url}}"
       ],
       "path": [
        "listings"
       ],
       "query": [
        {
         "key": "city",
         "value": "marseille-fos",
         "description": "City slug (abidjan, cotonou, dunkerque, los-angeles, marseille-fos, montreal, vancouver)"
        },
        {
         "key": "direction",
         "value": "offer",
         "description": "offer or need"
        },
        {
         "key": "per_page",
         "value": "20",
         "description": "Results per page (max 100)"
        },
        {
         "key": "container_type",
         "value": "40HC",
         "description": "Filter by container type",
         "disabled": true
        },
        {
         "key": "pickup_from",
         "value": "{{pw_start}}",
         "description": "Pickup window starting on/after this date (YYYY-MM-DD)",
         "disabled": true
        },
        {
         "key": "page",
         "value": "1",
         "disabled": true
        }
       ]
      },
      "description": "List published listings, newest first. Combine any of the query parameters — the disabled ones in this request show what else is available (tick them to activate).\n\nEach result carries its `id` — that is the value you use with `GET /listings/{id}`, `PATCH` and `DELETE`."
     },
     "response": [
      {
       "name": "200 — one page of results",
       "originalRequest": {
        "method": "GET",
        "header": [],
        "url": {
         "raw": "{{base_url}}/listings?city=marseille-fos&direction=offer&per_page=20",
         "host": [
          "{{base_url}}"
         ],
         "path": [
          "listings"
         ],
         "query": [
          {
           "key": "city",
           "value": "marseille-fos"
          },
          {
           "key": "direction",
           "value": "offer"
          },
          {
           "key": "per_page",
           "value": "20"
          }
         ]
        }
       },
       "status": "OK",
       "code": 200,
       "_postman_previewlanguage": "json",
       "header": [
        {
         "key": "Content-Type",
         "value": "application/json; charset=UTF-8"
        }
       ],
       "cookie": [],
       "body": "{\n  \"results\": [\n    {\n      \"id\": 10123,\n      \"url\": \"https://portchief.com/listing/offer-40hc-marseille-fos/\",\n      \"status\": \"publish\",\n      \"direction\": \"offer\",\n      \"title\": \"40HC available — Marseille-Fos terminal\",\n      \"description\": \"Five 40HC containers available for pickup at the Marseille-Fos terminal.\",\n      \"city\": { \"slug\": \"marseille-fos\", \"name\": \"Marseille-Fos\" },\n      \"container_type\": { \"slug\": \"40hc\", \"label\": \"40HC\" },\n      \"coords\": { \"lat\": 43.3, \"lng\": 5.37 },\n      \"address\": \"Marseille-Fos Port, France\",\n      \"pickup_window\": { \"start\": \"2026-07-23\", \"end\": \"2026-07-30\" },\n      \"price\": 350,\n      \"image_url\": \"https://portchief.com/wp-content/uploads/2026/07/portchief-container-40hc.webp\",\n      \"contact\": { \"email\": \"ops@yourcompany.com\", \"phone\": \"+33 4 00 00 00 00\", \"website\": null },\n      \"created_at\": \"2026-07-16T09:12:44+00:00\",\n      \"updated_at\": \"2026-07-16T09:12:44+00:00\"\n    }\n  ],\n  \"page\": 1,\n  \"per_page\": 20,\n  \"total\": 12,\n  \"total_pages\": 1\n}"
      }
     ]
    },
    {
     "name": "Search near coordinates",
     "event": [
      {
       "listen": "test",
       "script": {
        "type": "text/javascript",
        "exec": [
         "pm.test('Geo search OK (200)', () => pm.response.to.have.status(200));"
        ]
       }
      }
     ],
     "request": {
      "method": "GET",
      "header": [],
      "url": {
       "raw": "{{base_url}}/listings?lat=43.3&lng=5.37&radius_km=50",
       "host": [
        "{{base_url}}"
       ],
       "path": [
        "listings"
       ],
       "query": [
        {
         "key": "lat",
         "value": "43.3",
         "description": "Search center latitude"
        },
        {
         "key": "lng",
         "value": "5.37",
         "description": "Search center longitude"
        },
        {
         "key": "radius_km",
         "value": "50",
         "description": "Radius in kilometers"
        },
        {
         "key": "direction",
         "value": "offer",
         "disabled": true
        }
       ]
      },
      "description": "Geographic search: pass **all three** of `lat`, `lng` and `radius_km` (kilometers) to activate it — if one of them is missing, the geo mode is simply not applied (no error).\n\n**How results come back:** every published listing within the radius, **sorted by distance from your point, nearest first** — unlike the city search, which sorts by date. Want only the closest listing? Add `per_page=1`.\n\nEach result also carries **`distance_km`** — the distance from your search point (km, 1 decimal), so you can display “12.3 km away” or double-check the sort. The field exists **only** in geo mode: plain searches and *Get one listing* never have it. Combine freely with the other filters (`direction`, `city`, `container_type`, `mine`, pickup dates).\n\nTypical use: \"show me every offer within 50 km of my depot\"."
     },
     "response": [
      {
       "name": "200 — sorted by distance (nearest first)",
       "originalRequest": {
        "method": "GET",
        "header": [],
        "url": {
         "raw": "{{base_url}}/listings?lat=43.3&lng=5.37&radius_km=50",
         "host": [
          "{{base_url}}"
         ],
         "path": [
          "listings"
         ],
         "query": [
          {
           "key": "lat",
           "value": "43.3"
          },
          {
           "key": "lng",
           "value": "5.37"
          },
          {
           "key": "radius_km",
           "value": "50"
          }
         ]
        }
       },
       "status": "OK",
       "code": 200,
       "_postman_previewlanguage": "json",
       "header": [
        {
         "key": "Content-Type",
         "value": "application/json; charset=UTF-8"
        }
       ],
       "cookie": [],
       "body": "{\n  \"results\": [\n    {\n      \"id\": 10123,\n      \"distance_km\": 3.1,\n      \"url\": \"https://portchief.com/listing/offer-40hc-marseille-fos/\",\n      \"status\": \"publish\",\n      \"direction\": \"offer\",\n      \"title\": \"40HC available — Marseille-Fos terminal\",\n      \"description\": \"Five 40HC containers available for pickup at the Marseille-Fos terminal.\",\n      \"city\": {\n        \"slug\": \"marseille-fos\",\n        \"name\": \"Marseille-Fos\"\n      },\n      \"container_type\": {\n        \"slug\": \"40hc\",\n        \"label\": \"40HC\"\n      },\n      \"coords\": {\n        \"lat\": 43.32,\n        \"lng\": 5.36\n      },\n      \"address\": \"Marseille-Fos Port, France\",\n      \"pickup_window\": {\n        \"start\": \"2026-07-23\",\n        \"end\": \"2026-07-30\"\n      },\n      \"price\": 350,\n      \"image_url\": \"https://portchief.com/wp-content/uploads/2026/07/portchief-container-40hc.webp\",\n      \"contact\": {\n        \"email\": \"ops@yourcompany.com\",\n        \"phone\": \"+33 4 00 00 00 00\",\n        \"website\": null\n      },\n      \"created_at\": \"2026-07-16T09:12:44+00:00\",\n      \"updated_at\": \"2026-07-16T09:12:44+00:00\"\n    },\n    {\n      \"id\": 10098,\n      \"distance_km\": 18.7,\n      \"url\": \"https://portchief.com/listing/offer-20dc-marseille-fos/\",\n      \"status\": \"publish\",\n      \"direction\": \"offer\",\n      \"title\": \"20DC — inland depot near Vitrolles\",\n      \"description\": \"Two 20DC containers, empty, ready for pickup.\",\n      \"city\": {\n        \"slug\": \"marseille-fos\",\n        \"name\": \"Marseille-Fos\"\n      },\n      \"container_type\": {\n        \"slug\": \"20dc\",\n        \"label\": \"20DC\"\n      },\n      \"coords\": {\n        \"lat\": 43.46,\n        \"lng\": 5.25\n      },\n      \"address\": \"Vitrolles, France\",\n      \"pickup_window\": {\n        \"start\": null,\n        \"end\": null\n      },\n      \"price\": 200,\n      \"image_url\": null,\n      \"contact\": {\n        \"email\": \"ops@othercompany.com\",\n        \"phone\": null,\n        \"website\": null\n      },\n      \"created_at\": \"2026-07-14T15:40:02+00:00\",\n      \"updated_at\": \"2026-07-14T15:40:02+00:00\"\n    }\n  ],\n  \"page\": 1,\n  \"per_page\": 20,\n  \"total\": 2,\n  \"total_pages\": 1\n}"
      }
     ]
    },
    {
     "name": "My listings",
     "event": [
      {
       "listen": "test",
       "script": {
        "type": "text/javascript",
        "exec": [
         "pm.test('My listings OK (200)', () => pm.response.to.have.status(200));"
        ]
       }
      }
     ],
     "request": {
      "method": "GET",
      "header": [],
      "url": {
       "raw": "{{base_url}}/listings?mine=1",
       "host": [
        "{{base_url}}"
       ],
       "path": [
        "listings"
       ],
       "query": [
        {
         "key": "mine",
         "value": "1",
         "description": "Only listings owned by YOUR account (resolved from the token)"
        },
        {
         "key": "per_page",
         "value": "100",
         "disabled": true
        }
       ]
      },
      "description": "Your listings only — the account is read from the **token**, there is nothing else to pass.\n\nUse it to find an id you lost: every listing you still control is here, with its `id` (the remote control — see the collection intro). Withdrawn listings never appear: once withdrawn, a listing is gone from the public API (`GET`/`PATCH` on it return 404).\n\nCombine freely with the other filters and pagination, e.g. `mine=1&direction=offer`."
     },
     "response": [
      {
       "name": "200 — only your listings",
       "originalRequest": {
        "method": "GET",
        "header": [],
        "url": {
         "raw": "{{base_url}}/listings?mine=1",
         "host": [
          "{{base_url}}"
         ],
         "path": [
          "listings"
         ],
         "query": [
          {
           "key": "mine",
           "value": "1"
          }
         ]
        }
       },
       "status": "OK",
       "code": 200,
       "_postman_previewlanguage": "json",
       "header": [
        {
         "key": "Content-Type",
         "value": "application/json; charset=UTF-8"
        }
       ],
       "cookie": [],
       "body": "{\n  \"results\": [\n    {\n      \"id\": 10123,\n      \"url\": \"https://portchief.com/listing/offer-40hc-marseille-fos/\",\n      \"status\": \"publish\",\n      \"direction\": \"offer\",\n      \"title\": \"40HC available — Marseille-Fos terminal\",\n      \"description\": \"Five 40HC containers available for pickup at the Marseille-Fos terminal.\",\n      \"city\": {\n        \"slug\": \"marseille-fos\",\n        \"name\": \"Marseille-Fos\"\n      },\n      \"container_type\": {\n        \"slug\": \"40hc\",\n        \"label\": \"40HC\"\n      },\n      \"coords\": {\n        \"lat\": 43.32,\n        \"lng\": 5.36\n      },\n      \"address\": \"Marseille-Fos Port, France\",\n      \"pickup_window\": {\n        \"start\": \"2026-07-23\",\n        \"end\": \"2026-07-30\"\n      },\n      \"price\": 350,\n      \"image_url\": \"https://portchief.com/wp-content/uploads/2026/07/portchief-container-40hc.webp\",\n      \"contact\": {\n        \"email\": \"ops@yourcompany.com\",\n        \"phone\": \"+33 4 00 00 00 00\",\n        \"website\": null\n      },\n      \"created_at\": \"2026-07-16T09:12:44+00:00\",\n      \"updated_at\": \"2026-07-16T09:12:44+00:00\"\n    }\n  ],\n  \"page\": 1,\n  \"per_page\": 20,\n  \"total\": 1,\n  \"total_pages\": 1\n}"
      }
     ]
    },
    {
     "name": "Get one listing",
     "event": [
      {
       "listen": "test",
       "script": {
        "type": "text/javascript",
        "exec": [
         "pm.test('Listing found (200)', () => pm.response.to.have.status(200));"
        ]
       }
      }
     ],
     "request": {
      "method": "GET",
      "header": [],
      "url": {
       "raw": "{{base_url}}/listings/{{listing_id}}",
       "host": [
        "{{base_url}}"
       ],
       "path": [
        "listings",
        "{{listing_id}}"
       ]
      },
      "description": "Read one listing by `id` — the full shape, including contact details, coordinates, pickup window and image.\n\n`{{listing_id}}` is filled automatically when you create a listing with this collection; replace it with any id returned by the search."
     },
     "response": [
      {
       "name": "200 — full listing",
       "originalRequest": {
        "method": "GET",
        "header": [],
        "url": {
         "raw": "{{base_url}}/listings/10123",
         "host": [
          "{{base_url}}"
         ],
         "path": [
          "listings",
          "10123"
         ]
        }
       },
       "status": "OK",
       "code": 200,
       "_postman_previewlanguage": "json",
       "header": [
        {
         "key": "Content-Type",
         "value": "application/json; charset=UTF-8"
        }
       ],
       "cookie": [],
       "body": "{\n  \"id\": 10123,\n  \"url\": \"https://portchief.com/listing/offer-40hc-marseille-fos/\",\n  \"status\": \"publish\",\n  \"direction\": \"offer\",\n  \"title\": \"40HC available — Marseille-Fos terminal\",\n  \"description\": \"Five 40HC containers available for pickup at the Marseille-Fos terminal.\",\n  \"city\": { \"slug\": \"marseille-fos\", \"name\": \"Marseille-Fos\" },\n  \"container_type\": { \"slug\": \"40hc\", \"label\": \"40HC\" },\n  \"coords\": { \"lat\": 43.3, \"lng\": 5.37 },\n  \"address\": \"Marseille-Fos Port, France\",\n  \"pickup_window\": { \"start\": \"2026-07-23\", \"end\": \"2026-07-30\" },\n  \"price\": 350,\n  \"image_url\": \"https://portchief.com/wp-content/uploads/2026/07/portchief-container-40hc.webp\",\n  \"contact\": { \"email\": \"ops@yourcompany.com\", \"phone\": \"+33 4 00 00 00 00\", \"website\": null },\n  \"created_at\": \"2026-07-16T09:12:44+00:00\",\n  \"updated_at\": \"2026-07-16T09:12:44+00:00\"\n}"
      },
      {
       "name": "404 — unknown or withdrawn listing",
       "originalRequest": {
        "method": "GET",
        "header": [],
        "url": {
         "raw": "{{base_url}}/listings/999999",
         "host": [
          "{{base_url}}"
         ],
         "path": [
          "listings",
          "999999"
         ]
        }
       },
       "status": "Not Found",
       "code": 404,
       "_postman_previewlanguage": "json",
       "header": [
        {
         "key": "Content-Type",
         "value": "application/json; charset=UTF-8"
        }
       ],
       "cookie": [],
       "body": "{\n  \"code\": \"pc_not_found\",\n  \"message\": \"Listing not found.\",\n  \"data\": {\n    \"status\": 404\n  }\n}"
      }
     ]
    }
   ]
  },
  {
   "name": "4 · Update a listing",
   "description": "Partially update one of **your own** listings — send only the fields you want to change. Trying to update someone else's listing returns a 403.",
   "item": [
    {
     "name": "Update price",
     "event": [
      {
       "listen": "test",
       "script": {
        "type": "text/javascript",
        "exec": [
         "pm.test('Listing updated (200)', () => pm.response.to.have.status(200));"
        ]
       }
      }
     ],
     "request": {
      "method": "PATCH",
      "header": [
       {
        "key": "Content-Type",
        "value": "application/json"
       }
      ],
      "body": {
       "mode": "raw",
       "raw": "{\n  \"price\": 980\n}"
      },
      "url": {
       "raw": "{{base_url}}/listings/{{listing_id}}",
       "host": [
        "{{base_url}}"
       ],
       "path": [
        "listings",
        "{{listing_id}}"
       ]
      },
      "description": "PATCH is **partial**: only the fields present in the body are changed, everything else stays. All create fields are accepted (`title`, `description`, `price`, `city`, `container_type`, `direction`, `pickup_window`, `lat`/`lng` or `address`, `image_base64`).\n\nRules to know:\n- You can only update **your own** listings (403 otherwise).\n- Switching an offer to `direction: \"need\"` automatically resets its price to 0.\n- The response returns the full updated listing, so you can verify the change immediately."
     },
     "response": [
      {
       "name": "200 — updated listing returned",
       "originalRequest": {
        "method": "PATCH",
        "header": [
         {
          "key": "Content-Type",
          "value": "application/json"
         }
        ],
        "body": {
         "mode": "raw",
         "raw": "{\n  \"price\": 980\n}"
        },
        "url": {
         "raw": "{{base_url}}/listings/10123",
         "host": [
          "{{base_url}}"
         ],
         "path": [
          "listings",
          "10123"
         ]
        }
       },
       "status": "OK",
       "code": 200,
       "_postman_previewlanguage": "json",
       "header": [
        {
         "key": "Content-Type",
         "value": "application/json; charset=UTF-8"
        }
       ],
       "cookie": [],
       "body": "{\n  \"id\": 10123,\n  \"url\": \"https://portchief.com/listing/offer-40hc-marseille-fos/\",\n  \"status\": \"publish\",\n  \"direction\": \"offer\",\n  \"title\": \"40HC available — Marseille-Fos terminal\",\n  \"description\": \"Five 40HC containers available for pickup at the Marseille-Fos terminal.\",\n  \"city\": { \"slug\": \"marseille-fos\", \"name\": \"Marseille-Fos\" },\n  \"container_type\": { \"slug\": \"40hc\", \"label\": \"40HC\" },\n  \"coords\": { \"lat\": 43.3, \"lng\": 5.37 },\n  \"address\": \"Marseille-Fos Port, France\",\n  \"pickup_window\": { \"start\": \"2026-07-23\", \"end\": \"2026-07-30\" },\n  \"price\": 980,\n  \"image_url\": \"https://portchief.com/wp-content/uploads/2026/07/portchief-container-40hc.webp\",\n  \"contact\": { \"email\": \"ops@yourcompany.com\", \"phone\": \"+33 4 00 00 00 00\", \"website\": null },\n  \"created_at\": \"2026-07-16T09:12:44+00:00\",\n  \"updated_at\": \"2026-07-16T10:03:18+00:00\"\n}"
      },
      {
       "name": "403 — not your listing",
       "originalRequest": {
        "method": "PATCH",
        "header": [
         {
          "key": "Content-Type",
          "value": "application/json"
         }
        ],
        "body": {
         "mode": "raw",
         "raw": "{\n  \"price\": 980\n}"
        },
        "url": {
         "raw": "{{base_url}}/listings/10200",
         "host": [
          "{{base_url}}"
         ],
         "path": [
          "listings",
          "10200"
         ]
        }
       },
       "status": "Forbidden",
       "code": 403,
       "_postman_previewlanguage": "json",
       "header": [
        {
         "key": "Content-Type",
         "value": "application/json; charset=UTF-8"
        }
       ],
       "cookie": [],
       "body": "{\n  \"code\": \"pc_forbidden\",\n  \"message\": \"You can only modify your own listings.\",\n  \"data\": {\n    \"status\": 403\n  }\n}"
      }
     ]
    }
   ]
  },
  {
   "name": "5 · Withdraw a listing",
   "description": "Remove a listing from the marketplace when it is no longer relevant (containers gone, need fulfilled). Withdrawal is **soft**: the listing disappears from search and returns 404 on read, and the operation is **idempotent** — withdrawing the same listing twice is safe and still returns 200.",
   "item": [
    {
     "name": "Withdraw",
     "event": [
      {
       "listen": "test",
       "script": {
        "type": "text/javascript",
        "exec": [
         "pm.test('Listing withdrawn (200)', () => pm.response.to.have.status(200));",
         "pm.test('Status is withdrawn', () => pm.expect(pm.response.json().status).to.eql('withdrawn'));"
        ]
       }
      }
     ],
     "request": {
      "method": "DELETE",
      "header": [],
      "url": {
       "raw": "{{base_url}}/listings/{{listing_id}}",
       "host": [
        "{{base_url}}"
       ],
       "path": [
        "listings",
        "{{listing_id}}"
       ]
      },
      "description": "Withdraw one of **your own** listings by `id`.\n\n- After withdrawal the listing no longer appears in search and `GET`/`PATCH` return 404.\n- Idempotent: calling DELETE again on the same id still returns `200 { \"status\": \"withdrawn\" }`.\n- If you ran this whole collection top to bottom, this cleans up the listing you created in folder 2 (`{{listing_id}}` = the most recently created one; to withdraw the other, paste its id from the 201 response)."
     },
     "response": [
      {
       "name": "200 — withdrawn",
       "originalRequest": {
        "method": "DELETE",
        "header": [],
        "url": {
         "raw": "{{base_url}}/listings/10123",
         "host": [
          "{{base_url}}"
         ],
         "path": [
          "listings",
          "10123"
         ]
        }
       },
       "status": "OK",
       "code": 200,
       "_postman_previewlanguage": "json",
       "header": [
        {
         "key": "Content-Type",
         "value": "application/json; charset=UTF-8"
        }
       ],
       "cookie": [],
       "body": "{\n  \"id\": 10123,\n  \"status\": \"withdrawn\"\n}"
      },
      {
       "name": "403 — not your listing",
       "originalRequest": {
        "method": "DELETE",
        "header": [],
        "url": {
         "raw": "{{base_url}}/listings/10200",
         "host": [
          "{{base_url}}"
         ],
         "path": [
          "listings",
          "10200"
         ]
        }
       },
       "status": "Forbidden",
       "code": 403,
       "_postman_previewlanguage": "json",
       "header": [
        {
         "key": "Content-Type",
         "value": "application/json; charset=UTF-8"
        }
       ],
       "cookie": [],
       "body": "{\n  \"code\": \"pc_forbidden\",\n  \"message\": \"You can only modify your own listings.\",\n  \"data\": {\n    \"status\": 403\n  }\n}"
      }
     ]
    }
   ]
  }
 ]
}