{
  "info": {
    "_postman_id": "85dc1af2-0670-5516-971e-75f89c74aa18",
    "name": "Hotdog API",
    "description": "Every endpoint of the Hotdog API, with tests.\n\n**Quick start**\n1. Pick an environment (top right): **Hotdog API - Local** (`http://localhost:3000`) or **Hotdog API - Hosted** (edit `baseUrl` first).\n2. Open **1. Getting started > Get my free API key** and send it. The key is stored in `apiKey` and used by every other request.\n3. Send the requests one by one, or run the whole collection (**Run**): each request checks its answer.\n\n**Upload requests** need a file. Pick the file in the request (the samples are in `public/samples/`), or set Postman's working directory to the repository root. With Newman: `newman run public/postman/hotdog-api.postman_collection.json -e public/postman/local.postman_environment.json --working-dir .`\n\n**Variables** (collection > Variables): `apiKey` (filled for you), `keyName`, `remoteImageUrl` (public picture for `from_url`), `webhookUrl` (public https receiver; the default accepts any POST), `sandboxHotdogKey` / `demoKey` (public keys). Webhook receivers must be public: a local address is refused.\n\nA full run uses about 6 of your 100 daily classifications.",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "auth": {
    "type": "apikey",
    "apikey": [
      {
        "key": "key",
        "value": "X-API-Key",
        "type": "string"
      },
      {
        "key": "value",
        "value": "{{apiKey}}",
        "type": "string"
      },
      {
        "key": "in",
        "value": "header",
        "type": "string"
      }
    ]
  },
  "event": [
    {
      "listen": "prerequest",
      "script": {
        "type": "text/javascript",
        "exec": [
          "// Every call says which platform it comes from: GET /v1/usage then counts calls per source.",
          "if (!pm.request.headers.has('X-Integration-Source')) { pm.request.headers.add({ key: 'X-Integration-Source', value: 'postman' }); }",
          "// The hosted instance sleeps when nobody uses it. After a pause, wake it up first: ask /health until it",
          "// answers (about 15 s at most), so that the request itself does not meet a 502 from the platform.",
          "const base = pm.variables.replaceIn('{{baseUrl}}');",
          "const wake = (attempt) => pm.sendRequest({ url: base + '/health', method: 'GET' }, (error, response) => {",
          "  if ((error || !response || response.code !== 200) && attempt < 6) { setTimeout(() => wake(attempt + 1), 2500); }",
          "});",
          "if (Date.now() - Number(pm.collectionVariables.get('lastCallAt') || 0) > 240000) { wake(1); }",
          "pm.collectionVariables.set('lastCallAt', String(Date.now()));"
        ]
      }
    }
  ],
  "item": [
    {
      "name": "1. Getting started",
      "description": "Check the API, get your own free key, check that it works.",
      "item": [
        {
          "name": "Ping",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/ping",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "ping"
              ]
            },
            "description": "Needs no key. The same call is handy as an uptime check.",
            "auth": {
              "type": "noauth"
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status is 200\", () => pm.response.to.have.status(200));",
                  "pm.test(\"Answers with a message\", () => pm.expect(pm.response.json().message).to.be.a(\"string\"));"
                ]
              }
            }
          ]
        },
        {
          "name": "Get my free API key",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/keys",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "keys"
              ]
            },
            "description": "No account. The key is shown ONCE: this request stores it in the `apiKey` variable, which every other request uses.\n\nAlready have a key? Skip this request and paste it into the `apiKey` collection variable.",
            "auth": {
              "type": "noauth"
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"{{keyName}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status is 201\", () => pm.response.to.have.status(201));",
                  "const body = pm.response.json();",
                  "pm.test(\"A key starting with hd_ is given\", () => pm.expect(body.api_key).to.match(/^hd_[0-9a-f]{48}$/));",
                  "pm.test(\"Plan is free, 100 a day\", () => { pm.expect(body.plan).to.eql(\"free\"); pm.expect(body.quota.limit).to.eql(100); });",
                  "pm.collectionVariables.set(\"apiKey\", body.api_key);"
                ]
              }
            }
          ]
        },
        {
          "name": "Check my key",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/me",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "me"
              ]
            },
            "description": "Use this as the connection test of a Zapier, Make or n8n app: 200 with `name`, `plan` and `quota`, or 401."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status is 200\", () => pm.response.to.have.status(200));",
                  "const body = pm.response.json();",
                  "pm.test(\"Shows the plan and the quota\", () => { pm.expect(body.plan).to.eql(\"free\"); pm.expect(body.quota.remaining).to.be.at.most(100); });"
                ]
              }
            }
          ]
        },
        {
          "name": "Check my key, sent as a bearer token",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/me",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "me"
              ]
            },
            "description": "The same key can be sent as `Authorization: Bearer <key>` instead of `X-API-Key`. Use whichever your platform prefers.",
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{apiKey}}",
                  "type": "string"
                }
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status is 200\", () => pm.response.to.have.status(200));",
                  "pm.test(\"Same answer with a bearer token\", () => pm.expect(pm.response.json().plan).to.eql(\"free\"));"
                ]
              }
            }
          ]
        },
        {
          "name": "Sample images",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/samples",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "samples"
              ]
            },
            "description": "Pictures to try, with the expected verdict. Served under `/samples/`.",
            "auth": {
              "type": "noauth"
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status is 200\", () => pm.response.to.have.status(200));",
                  "pm.test(\"Several samples, hot dogs and others\", () => { const d = pm.response.json().data; pm.expect(d.length).to.be.at.least(4); pm.expect(d.map(s => s.hotdog)).to.include.members([true, false]); });"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "2. Classification",
      "description": "Is it a hot dog? By upload or by URL. Each classification uses one of today's requests.",
      "item": [
        {
          "name": "Classify an uploaded image",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/classifications",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "classifications"
              ]
            },
            "description": "Multipart, file field `image`. **Select the file**: in Postman pick `public/samples/hotdog-1.jpg` (or any picture); with Newman, run from the repository root (`--working-dir .`).",
            "body": {
              "mode": "formdata",
              "formdata": [
                {
                  "key": "image",
                  "type": "file",
                  "src": "public/samples/hotdog-1.jpg"
                },
                {
                  "key": "label",
                  "value": "from Postman",
                  "type": "text"
                }
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status is 201\", () => pm.response.to.have.status(201));",
                  "const body = pm.response.json();",
                  "pm.test(\"A hot dog, recognised by the real model\", () => { pm.expect(body.hotdog).to.eql(true); pm.expect(body.top_label).to.eql(\"hotdog\"); pm.expect(body.confidence).to.be.above(0.9); });",
                  "pm.test(\"Recorded in the history\", () => { pm.expect(body.id).to.be.a(\"number\"); pm.expect(body.label).to.eql(\"from Postman\"); pm.expect(body.source).to.eql(\"upload\"); });",
                  "pm.test(\"The remaining quota is advertised\", () => pm.expect(pm.response.headers.get(\"RateLimit-Remaining\")).to.not.be.null);",
                  "pm.collectionVariables.set(\"classificationId\", body.id);"
                ]
              }
            }
          ]
        },
        {
          "name": "Classify an image from a URL",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/classifications/from_url",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "classifications",
                "from_url"
              ]
            },
            "description": "The server downloads the image: easier than a file upload in an automation platform. Public `http(s)` only; private and local addresses are refused.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"url\": \"{{remoteImageUrl}}\",\n  \"label\": \"from a URL\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status is 201\", () => pm.response.to.have.status(201));",
                  "const body = pm.response.json();",
                  "pm.test(\"Comes from a URL\", () => { pm.expect(body.source).to.eql(\"url\"); pm.expect(body.hotdog).to.be.a(\"boolean\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "Classify with a sandbox key (always a hot dog)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{sandboxHotdogKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/classifications",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "classifications"
              ]
            },
            "description": "Sandbox keys give a fixed verdict whatever the picture, to practise without spending your quota. The image is still validated.",
            "auth": {
              "type": "noauth"
            },
            "body": {
              "mode": "formdata",
              "formdata": [
                {
                  "key": "image",
                  "type": "file",
                  "src": "public/samples/cat-1.jpg"
                }
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status is 201\", () => pm.response.to.have.status(201));",
                  "const body = pm.response.json();",
                  "pm.test(\"Says hot dog for a cat\", () => { pm.expect(body.hotdog).to.eql(true); pm.expect(body.top_label).to.eql(\"hotdog\"); });",
                  "pm.collectionVariables.set(\"sandboxEntryId\", body.id);"
                ]
              }
            }
          ]
        },
        {
          "name": "Remove that sandbox entry",
          "request": {
            "method": "DELETE",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{sandboxHotdogKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/classifications/{{sandboxEntryId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "classifications",
                "{{sandboxEntryId}}"
              ]
            },
            "description": "The sandbox key is shared: tidy up after yourself.",
            "auth": {
              "type": "noauth"
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status is 204\", () => pm.response.to.have.status(204));"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "3. History",
      "description": "The verdicts you obtained. The images themselves are never stored.",
      "item": [
        {
          "name": "List my history",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/classifications?limit=5",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "classifications"
              ],
              "query": [
                {
                  "key": "limit",
                  "value": "5"
                }
              ]
            },
            "description": "Newest first, with a cursor: `?limit=` (1-100) and `?after=<next_cursor>`."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status is 200\", () => pm.response.to.have.status(200));",
                  "const body = pm.response.json();",
                  "pm.test(\"A page of entries, newest first\", () => { const ids = body.data.map(e => e.id); pm.expect(body.data).to.be.an(\"array\"); pm.expect(body).to.have.property(\"next_cursor\"); pm.expect(ids).to.eql([...ids].sort((a, b) => b - a)); });",
                  "pm.test(\"Contains the upload from step 2\", () => pm.expect(body.data.map(e => e.id)).to.include(Number(pm.collectionVariables.get(\"classificationId\"))));"
                ]
              }
            }
          ]
        },
        {
          "name": "Only the hot dogs",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/classifications?hotdog=true",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "classifications"
              ],
              "query": [
                {
                  "key": "hotdog",
                  "value": "true"
                }
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status is 200\", () => pm.response.to.have.status(200));",
                  "pm.test(\"Only hot dogs\", () => pm.expect(pm.response.json().data.every(e => e.hotdog === true)).to.be.true);"
                ]
              }
            }
          ]
        },
        {
          "name": "Get one entry",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/classifications/{{classificationId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "classifications",
                "{{classificationId}}"
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status is 200\", () => pm.response.to.have.status(200));",
                  "pm.test(\"The right entry\", () => pm.expect(pm.response.json().id).to.eql(Number(pm.collectionVariables.get(\"classificationId\"))));"
                ]
              }
            }
          ]
        },
        {
          "name": "Rename an entry",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/classifications/{{classificationId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "classifications",
                "{{classificationId}}"
              ]
            },
            "description": "Only the free-form `label` can change.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"label\": \"renamed from Postman\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status is 200\", () => pm.response.to.have.status(200));",
                  "pm.test(\"Label changed, verdict untouched\", () => { const b = pm.response.json(); pm.expect(b.label).to.eql(\"renamed from Postman\"); pm.expect(b.hotdog).to.eql(true); });"
                ]
              }
            }
          ]
        },
        {
          "name": "Delete an entry",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/classifications/{{classificationId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "classifications",
                "{{classificationId}}"
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status is 204\", () => pm.response.to.have.status(204));"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "4. Hot dogs and spots",
      "description": "Your own collection: a complete CRUD with filters, cursor pagination, a dropdown list (spots) and a nested object.",
      "item": [
        {
          "name": "Create a spot",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/spots",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "spots"
              ]
            },
            "description": "A place that sells hot dogs. Spots feed a dropdown (a dynamic select in Make and Zapier).",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"Lou's Hot Dogs\",\n  \"city\": \"Chicago\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status is 201\", () => pm.response.to.have.status(201));",
                  "const body = pm.response.json();",
                  "pm.test(\"Created\", () => { pm.expect(body.name).to.eql(\"Lou's Hot Dogs\"); pm.expect(body.city).to.eql(\"Chicago\"); });",
                  "pm.collectionVariables.set(\"spotId\", body.id);"
                ]
              }
            }
          ]
        },
        {
          "name": "List spots (a dropdown)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/spots?limit=50",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "spots"
              ],
              "query": [
                {
                  "key": "limit",
                  "value": "50"
                }
              ]
            },
            "description": "`?city=` and `?q=` filter. Newest first, cursor-paginated like every list."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status is 200\", () => pm.response.to.have.status(200));",
                  "pm.test(\"Contains my spot\", () => pm.expect(pm.response.json().data.map(s => s.id)).to.include(Number(pm.collectionVariables.get(\"spotId\"))));"
                ]
              }
            }
          ]
        },
        {
          "name": "Get one spot",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/spots/{{spotId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "spots",
                "{{spotId}}"
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status is 200\", () => pm.response.to.have.status(200));",
                  "pm.test(\"The right one\", () => pm.expect(pm.response.json().id).to.eql(Number(pm.collectionVariables.get(\"spotId\"))));"
                ]
              }
            }
          ]
        },
        {
          "name": "Create a hot dog with its spot",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/hotdogs",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "hotdogs"
              ]
            },
            "description": "The answer nests the spot: `spot: { id, name, city }`.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"Coney\",\n  \"style\": \"coney_island\",\n  \"rating\": 3,\n  \"spot_id\": \"{{spotId}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status is 201\", () => pm.response.to.have.status(201));",
                  "const body = pm.response.json();",
                  "pm.test(\"The spot is nested in the answer\", () => { pm.expect(body.spot).to.be.an(\"object\"); pm.expect(body.spot.name).to.eql(\"Lou's Hot Dogs\"); pm.expect(body.spot_id).to.eql(Number(pm.collectionVariables.get(\"spotId\"))); });",
                  "pm.collectionVariables.set(\"hotdogId2\", body.id);"
                ]
              }
            }
          ]
        },
        {
          "name": "Create a hot dog",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/hotdogs",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "hotdogs"
              ]
            },
            "description": "`style`: chicago, coney_island, corn_dog, new_york, sonoran, seattle, german or other (default). `rating`: 1 to 5. JSON or form data.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"Windy City\",\n  \"style\": \"chicago\",\n  \"toppings\": [\n    \"mustard\",\n    \"relish\"\n  ],\n  \"rating\": 5\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status is 201\", () => pm.response.to.have.status(201));",
                  "const body = pm.response.json();",
                  "pm.test(\"Created with its fields\", () => { pm.expect(body.name).to.eql(\"Windy City\"); pm.expect(body.toppings).to.eql([\"mustard\", \"relish\"]); pm.expect(body.rating).to.eql(5); });",
                  "pm.collectionVariables.set(\"hotdogId\", body.id);"
                ]
              }
            }
          ]
        },
        {
          "name": "Create a second hot dog",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/hotdogs",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "hotdogs"
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"Coney\",\n  \"style\": \"coney_island\",\n  \"rating\": 3\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status is 201\", () => pm.response.to.have.status(201));",
                  "pm.collectionVariables.set(\"hotdogId3\", pm.response.json().id);"
                ]
              }
            }
          ]
        },
        {
          "name": "List, first page of 1",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/hotdogs?limit=1",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "hotdogs"
              ],
              "query": [
                {
                  "key": "limit",
                  "value": "1"
                }
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status is 200\", () => pm.response.to.have.status(200));",
                  "const body = pm.response.json();",
                  "pm.test(\"One item and a cursor to the next page\", () => { pm.expect(body.data.length).to.eql(1); pm.expect(body.next_cursor).to.be.a(\"string\"); });",
                  "pm.collectionVariables.set(\"cursor\", body.next_cursor);"
                ]
              }
            }
          ]
        },
        {
          "name": "List, next page",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/hotdogs?limit=1&after={{cursor}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "hotdogs"
              ],
              "query": [
                {
                  "key": "limit",
                  "value": "1"
                },
                {
                  "key": "after",
                  "value": "{{cursor}}"
                }
              ]
            },
            "description": "Pass the previous `next_cursor` as `after`. The last page has `next_cursor: null`."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status is 200\", () => pm.response.to.have.status(200));",
                  "const body = pm.response.json();",
                  "pm.test(\"The other hot dog, then the end\", () => { pm.expect(body.data.length).to.eql(1); pm.expect(body.next_cursor).to.not.be.undefined; });"
                ]
              }
            }
          ]
        },
        {
          "name": "Filter by style, rating, name and spot",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/hotdogs?style=chicago&min_rating=4&q=windy",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "hotdogs"
              ],
              "query": [
                {
                  "key": "style",
                  "value": "chicago"
                },
                {
                  "key": "min_rating",
                  "value": "4"
                },
                {
                  "key": "q",
                  "value": "windy"
                }
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status is 200\", () => pm.response.to.have.status(200));",
                  "pm.test(\"Only the Chicago one\", () => { const d = pm.response.json().data; pm.expect(d.length).to.eql(1); pm.expect(d[0].style).to.eql(\"chicago\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "Only what is new (polling)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/hotdogs?created_after={{since}}&limit=10",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "hotdogs"
              ],
              "query": [
                {
                  "key": "created_after",
                  "value": "{{since}}"
                },
                {
                  "key": "limit",
                  "value": "10"
                }
              ]
            },
            "description": "`created_after` (ISO 8601) keeps what was created after that moment: what a polling trigger asks for. Dates are always ISO 8601."
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.collectionVariables.set(\"since\", new Date(Date.now() - 3600 * 1000).toISOString());"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status is 200\", () => pm.response.to.have.status(200));",
                  "pm.test(\"Contains what I just created\", () => pm.expect(pm.response.json().data.map(h => h.id)).to.include(Number(pm.collectionVariables.get(\"hotdogId\"))));"
                ]
              }
            }
          ]
        },
        {
          "name": "Get one hot dog",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/hotdogs/{{hotdogId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "hotdogs",
                "{{hotdogId}}"
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status is 200\", () => pm.response.to.have.status(200));",
                  "pm.test(\"The right one\", () => pm.expect(pm.response.json().id).to.eql(Number(pm.collectionVariables.get(\"hotdogId\"))));"
                ]
              }
            }
          ]
        },
        {
          "name": "Update a hot dog",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/hotdogs/{{hotdogId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "hotdogs",
                "{{hotdogId}}"
              ]
            },
            "description": "Only the fields you send change.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"rating\": 4\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status is 200\", () => pm.response.to.have.status(200));",
                  "pm.test(\"Rating changed, name kept\", () => { const b = pm.response.json(); pm.expect(b.rating).to.eql(4); pm.expect(b.name).to.eql(\"Windy City\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "Delete a hot dog",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/hotdogs/{{hotdogId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "hotdogs",
                "{{hotdogId}}"
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status is 204\", () => pm.response.to.have.status(204));"
                ]
              }
            }
          ]
        },
        {
          "name": "Delete the second hot dog",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/hotdogs/{{hotdogId2}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "hotdogs",
                "{{hotdogId2}}"
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status is 204\", () => pm.response.to.have.status(204));"
                ]
              }
            }
          ]
        },
        {
          "name": "Delete the third hot dog",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/hotdogs/{{hotdogId3}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "hotdogs",
                "{{hotdogId3}}"
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status is 204\", () => pm.response.to.have.status(204));"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "5. Webhooks",
      "description": "Be told when a classification is completed. The receiving URL must be public https (see the `webhookUrl` variable).",
      "item": [
        {
          "name": "Subscribe a URL",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/webhooks",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "webhooks"
              ]
            },
            "description": "`events` chooses what to receive (`classification.completed`, `hotdog.created`, `hotdog.updated`, `hotdog.deleted`); without it, everything. The `secret` signs the events (`X-Hotdog-Signature`) and is shown ONLY in this answer. At most 5 webhooks per key, with your own key (not the shared ones).",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"url\": \"{{webhookUrl}}\",\n  \"events\": [\n    \"hotdog.created\"\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status is 201\", () => pm.response.to.have.status(201));",
                  "const body = pm.response.json();",
                  "pm.test(\"The chosen event and the secret\", () => { pm.expect(body.events).to.eql([\"hotdog.created\"]); pm.expect(body.secret).to.match(/^whsec_/); });",
                  "pm.collectionVariables.set(\"webhookId\", body.id);",
                  "pm.collectionVariables.set(\"webhookSecret\", body.secret);"
                ]
              }
            }
          ]
        },
        {
          "name": "List my webhooks",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/webhooks",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "webhooks"
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status is 200\", () => pm.response.to.have.status(200));",
                  "pm.test(\"Contains the new one, without its secret\", () => { const d = pm.response.json().data; const mine = d.find(w => w.id === Number(pm.collectionVariables.get(\"webhookId\"))); pm.expect(mine).to.exist; pm.expect(mine).to.not.have.property(\"secret\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "Get one webhook",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/webhooks/{{webhookId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "webhooks",
                "{{webhookId}}"
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status is 200\", () => pm.response.to.have.status(200));",
                  "pm.test(\"No secret this time\", () => pm.expect(pm.response.json()).to.not.have.property(\"secret\"));"
                ]
              }
            }
          ]
        },
        {
          "name": "Send a test event",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/webhooks/{{webhookId}}/test",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "webhooks",
                "{{webhookId}}",
                "test"
              ]
            },
            "description": "Delivers a `webhook.test` event right now (counts as one request). With a receiver that answers 2xx the status is `succeeded`; otherwise `failed`, with the reason in `error`."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status is 200\", () => pm.response.to.have.status(200));",
                  "const body = pm.response.json();",
                  "pm.test(\"The delivery is reported\", () => { pm.expect(body.event).to.eql(\"webhook.test\"); pm.expect([\"succeeded\", \"failed\"]).to.include(body.status); });"
                ]
              }
            }
          ]
        },
        {
          "name": "Create a hot dog (fires hotdog.created)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/hotdogs",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "hotdogs"
              ]
            },
            "description": "Every hot dog created queues a `hotdog.created` event for the webhooks that listen to it. Updating and deleting send `hotdog.updated` and `hotdog.deleted`.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"Fires the webhook\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status is 201\", () => pm.response.to.have.status(201));",
                  "pm.collectionVariables.set(\"hotdogId4\", pm.response.json().id);"
                ]
              }
            }
          ]
        },
        {
          "name": "See the delivery log",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/webhooks/{{webhookId}}/deliveries",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "webhooks",
                "{{webhookId}}",
                "deliveries"
              ]
            },
            "description": "Newest first. The event queued above is delivered in the background, so it may still be `pending` for a moment."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status is 200\", () => pm.response.to.have.status(200));",
                  "pm.test(\"The test event is logged\", () => { const d = pm.response.json().data; pm.expect(d.map(x => x.event)).to.include(\"webhook.test\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "Unsubscribe",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/webhooks/{{webhookId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "webhooks",
                "{{webhookId}}"
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status is 204\", () => pm.response.to.have.status(204));"
                ]
              }
            }
          ]
        },
        {
          "name": "Delete that hot dog",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/hotdogs/{{hotdogId4}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "hotdogs",
                "{{hotdogId4}}"
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status is 204\", () => pm.response.to.have.status(204));"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "6. What goes wrong",
      "description": "Every error has the same shape: `{\"error\": {\"code\", \"message\"}}`. Branch on `code`, show `message`.",
      "item": [
        {
          "name": "No key",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/me",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "me"
              ]
            },
            "auth": {
              "type": "noauth"
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status is 401\", () => pm.response.to.have.status(401));",
                  "pm.test(\"missing_api_key\", () => pm.expect(pm.response.json().error.code).to.eql(\"missing_api_key\"));"
                ]
              }
            }
          ]
        },
        {
          "name": "Wrong key",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-API-Key",
                "value": "hd_not_a_real_key",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/me",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "me"
              ]
            },
            "auth": {
              "type": "noauth"
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status is 401\", () => pm.response.to.have.status(401));",
                  "pm.test(\"invalid_api_key\", () => pm.expect(pm.response.json().error.code).to.eql(\"invalid_api_key\"));"
                ]
              }
            }
          ]
        },
        {
          "name": "A text file is not an image",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/classifications",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "classifications"
              ]
            },
            "description": "Select any non-image file. The format is read from the content, never from the name.",
            "body": {
              "mode": "formdata",
              "formdata": [
                {
                  "key": "image",
                  "type": "file",
                  "src": "README.md"
                }
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status is 415\", () => pm.response.to.have.status(415));",
                  "pm.test(\"unsupported_image_format\", () => pm.expect(pm.response.json().error.code).to.eql(\"unsupported_image_format\"));"
                ]
              }
            }
          ]
        },
        {
          "name": "No image at all",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/classifications",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "classifications"
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status is 400\", () => pm.response.to.have.status(400));",
                  "pm.test(\"parameter_missing\", () => pm.expect(pm.response.json().error.code).to.eql(\"parameter_missing\"));"
                ]
              }
            }
          ]
        },
        {
          "name": "A private address is refused",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/classifications/from_url",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "classifications",
                "from_url"
              ]
            },
            "description": "The server will not fetch internal addresses on your behalf (SSRF protection).",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"url\": \"http://169.254.169.254/latest/meta-data/\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status is 422\", () => pm.response.to.have.status(422));",
                  "pm.test(\"url_not_allowed\", () => pm.expect(pm.response.json().error.code).to.eql(\"url_not_allowed\"));"
                ]
              }
            }
          ]
        },
        {
          "name": "Only http and https URLs",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/classifications/from_url",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "classifications",
                "from_url"
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"url\": \"file:///etc/passwd\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status is 422\", () => pm.response.to.have.status(422));",
                  "pm.test(\"invalid_url\", () => pm.expect(pm.response.json().error.code).to.eql(\"invalid_url\"));"
                ]
              }
            }
          ]
        },
        {
          "name": "Invalid fields are explained",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/hotdogs",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "hotdogs"
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"style\": \"pizza\",\n  \"rating\": 9\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status is 422\", () => pm.response.to.have.status(422));",
                  "const error = pm.response.json().error;",
                  "pm.test(\"validation_failed, field by field\", () => { pm.expect(error.code).to.eql(\"validation_failed\"); pm.expect(Object.keys(error.details)).to.have.members([\"name\", \"style\", \"rating\"]); });"
                ]
              }
            }
          ]
        },
        {
          "name": "Not found",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/hotdogs/0",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "hotdogs",
                "0"
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status is 404\", () => pm.response.to.have.status(404));",
                  "pm.test(\"not_found\", () => pm.expect(pm.response.json().error.code).to.eql(\"not_found\"));"
                ]
              }
            }
          ]
        },
        {
          "name": "Get a 429 on purpose",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/errors/429",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "errors",
                "429"
              ]
            },
            "description": "Hard to obtain otherwise, so the API simulates it: `/v1/errors/{status}` for 400, 401, 403, 404, 422, 429 (with `Retry-After: 2`), 500 and 503. Use it to test how your module retries or reports failures."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status is 429\", () => pm.response.to.have.status(429));",
                  "pm.test(\"rate_limited, retry after 2 seconds\", () => { pm.expect(pm.response.json().error.code).to.eql(\"rate_limited\"); pm.expect(pm.response.headers.get(\"Retry-After\")).to.eql(\"2\"); });"
                ]
              }
            }
          ]
        },
        {
          "name": "Get a 500 on purpose",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/errors/500",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "errors",
                "500"
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status is 500\", () => pm.response.to.have.status(500));",
                  "pm.test(\"internal_error\", () => pm.expect(pm.response.json().error.code).to.eql(\"internal_error\"));"
                ]
              }
            }
          ]
        },
        {
          "name": "A sandbox key cannot use webhooks",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{sandboxHotdogKey}}",
                "type": "text"
              },
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/webhooks",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "webhooks"
              ]
            },
            "description": "The shared keys are public: through them anybody could make the server call any address, so webhooks need your own key.",
            "auth": {
              "type": "noauth"
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"url\": \"{{webhookUrl}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status is 403\", () => pm.response.to.have.status(403));",
                  "pm.test(\"key_not_allowed\", () => pm.expect(pm.response.json().error.code).to.eql(\"key_not_allowed\"));"
                ]
              }
            }
          ]
        },
        {
          "name": "The shared demo key cannot be reset",
          "request": {
            "method": "DELETE",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{demoKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/me/data",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "me",
                "data"
              ]
            },
            "auth": {
              "type": "noauth"
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status is 403\", () => pm.response.to.have.status(403));",
                  "pm.test(\"key_not_allowed\", () => pm.expect(pm.response.json().error.code).to.eql(\"key_not_allowed\"));"
                ]
              }
            }
          ]
        },
        {
          "name": "A name is required to sign up",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/keys",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "keys"
              ]
            },
            "auth": {
              "type": "noauth"
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status is 422\", () => pm.response.to.have.status(422));",
                  "pm.test(\"validation_failed\", () => pm.expect(pm.response.json().error.code).to.eql(\"validation_failed\"));"
                ]
              }
            }
          ]
        },
        {
          "name": "The shared demo key cannot be revoked",
          "request": {
            "method": "DELETE",
            "header": [
              {
                "key": "X-API-Key",
                "value": "{{demoKey}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/me",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "me"
              ]
            },
            "auth": {
              "type": "noauth"
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status is 403\", () => pm.response.to.have.status(403));",
                  "pm.test(\"key_not_revocable\", () => pm.expect(pm.response.json().error.code).to.eql(\"key_not_revocable\"));"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "7. Measure, reset, clean up",
      "description": "See your calls per platform, start again from scratch, and revoke the key made for this run.",
      "item": [
        {
          "name": "My calls per platform",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/usage",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "usage"
              ]
            },
            "description": "Counts your calls under each `X-Integration-Source` value (this collection sends `postman`), and how many failed. This is how the channel is measured."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status is 200\", () => pm.response.to.have.status(200));",
                  "const body = pm.response.json();",
                  "pm.test(\"My calls are counted under postman, with the errors of this run\", () => { const mine = body.data.find(s => s.source === \"postman\"); pm.expect(mine).to.exist; pm.expect(mine.requests).to.be.above(10); pm.expect(mine.errors).to.be.above(0); });"
                ]
              }
            }
          ]
        },
        {
          "name": "Everything my key stored",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "X-Integration-Source",
                "value": "interface-sync",
                "description": "Not counted in the usage figures, so that polling does not change them."
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/me/data",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "me",
                "data"
              ]
            },
            "description": "Your hot dogs, spots, history, webhooks and usage figures in one call (100 entries per list at most). The answer has an `ETag`: send it back as `If-None-Match` to get an empty 304 while nothing has changed. This is what the home page shows in \"My data\"."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status is 200\", () => pm.response.to.have.status(200));",
                  "const body = pm.response.json();",
                  "pm.test(\"Carries every list and an ETag\", () => { ['hotdogs', 'spots', 'classifications', 'webhooks'].forEach((name) => pm.expect(body[name].data).to.be.an('array')); pm.expect(body.usage.data).to.be.an('array'); pm.response.to.have.header('ETag'); });"
                ]
              }
            }
          ]
        },
        {
          "name": "Reset my data",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/me/data",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "me",
                "data"
              ]
            },
            "description": "Deletes your hot dogs, spots, history, webhooks and usage figures, and keeps your key and its quota: to start an exercise again."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status is 204\", () => pm.response.to.have.status(204));"
                ]
              }
            }
          ]
        },
        {
          "name": "Everything is gone, the key still works",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/hotdogs",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "hotdogs"
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status is 200\", () => pm.response.to.have.status(200));",
                  "pm.test(\"No hot dog left\", () => pm.expect(pm.response.json().data).to.eql([]));"
                ]
              }
            }
          ]
        },
        {
          "name": "Revoke my key",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/me",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "me"
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status is 204\", () => pm.response.to.have.status(204));"
                ]
              }
            }
          ]
        },
        {
          "name": "The revoked key no longer works",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/me",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "me"
              ]
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test(\"Status is 401\", () => pm.response.to.have.status(401));",
                  "pm.test(\"invalid_api_key\", () => pm.expect(pm.response.json().error.code).to.eql(\"invalid_api_key\"));"
                ]
              }
            }
          ]
        }
      ]
    }
  ],
  "variable": [
    {
      "key": "baseUrl",
      "value": "http://localhost:3000",
      "description": "Overridden by the environment"
    },
    {
      "key": "apiKey",
      "value": "",
      "description": "Filled by 'Get my free API key', or paste your own"
    },
    {
      "key": "keyName",
      "value": "Postman",
      "description": "Name given when signing up"
    },
    {
      "key": "spotId",
      "value": "",
      "description": "Filled by 'Create a spot'"
    },
    {
      "key": "remoteImageUrl",
      "value": "https://raw.githubusercontent.com/github/explore/main/topics/ruby/ruby.png",
      "description": "A public picture, for from_url"
    },
    {
      "key": "webhookUrl",
      "value": "https://httpbin.org/post",
      "description": "A public https URL that accepts POST requests"
    },
    {
      "key": "sandboxHotdogKey",
      "value": "hd_test_hotdog_000000000000000000000000000000",
      "description": "Public key: always a hot dog"
    },
    {
      "key": "demoKey",
      "value": "hd_demo_0000000000000000000000000000000000000000",
      "description": "Public shared demo key"
    }
  ]
}
