# UK bin collection dates for apps and AI agents Find upcoming household rubbish, recycling, food waste and garden waste collection dates using a UK postcode and address. WhenIsBins checks councils' own published information and returns JSON containing collection dates, waste types, council source links and information about address matching and date confidence. WhenIsBins is an independent service, not a council. Start without an account or API key, within the [free anonymous limits](#free-allowances). For regular use, request a free token with higher allowances. Coverage and available dates vary by council and property. Some lookups finish immediately; others require an asynchronous council check. Results can include dates up to 92 days ahead, but a full calendar is not always available. - [Agent quickstart](#agent-quickstart) — get from a postcode to an answer. - [Complete JavaScript example](#complete-javascript-example) — discover, select, submit and wait. - [Markdown guide](https://whenisbins.com/v1/guide.md) — this same guide for HTTP tools. - [OpenAPI contract](https://whenisbins.com/v1/openapi.yaml) — exact request and response schemas. - [Browsable reference](https://whenisbins.com/v1/docs) — explore the contract. - [JavaScript client](https://whenisbins.com/v1/client.mjs) — dependency-free Node 22+ client. - [OpenClaw skill](https://whenisbins.com/v1/openclaw-skill.md) — agent instructions and package guidance. ## When to use this API Use WhenIsBins when a user asks "When is my next bin collection?", "Which bin is collected this week?" or "When is my recycling collected?" It supports apps, home automation and AI assistants that can make HTTP GET and POST requests. Start with the user's full UK postcode. The API tells you which address details the collecting council needs. Ask for missing information or an exact selection when necessary; a postcode alone may not identify the home. For bank-holiday questions, use the returned dates and qualifications. Do not calculate an assumed timetable change. Use [council coverage](https://whenisbins.com/v1/councils) to check supported authorities; coverage is not a guarantee for every property. This API retrieves household collection information. It does not report missed bins, create reminders, arrange collections, serve commercial waste enquiries, or establish whether a household subscribes to garden waste. ## Agent quickstart The base URL is `https://whenisbins.com/v1`. Occasional queries need no token, registration or browser challenge. [Limits and retries](#free-allowances) apply to anonymous and token clients, including requests that start new work. 1. Discover: call `GET /addresses?postcode=...` with a URL-encoded postcode. Read `required_input`, `candidates` and `input_options`. 2. Resolve: use the user's exact, unambiguous address or ask them to select a candidate. Follow `required_input` for other council journeys. Never guess a property or consent to a neighbouring-property answer on the user's behalf. 3. Submit once: save the selected JSON and a new `Idempotency-Key` privately, then send them to `POST /lookups`. Reuse both unchanged after a lost response. 4. Inspect: HTTP `201` means the Lookup was created, not necessarily completed. `done` and `failed` are terminal. `queued`, `running` and `partial` need continuation. 5. Continue: poll the same lookup ID, respecting `Retry-After` and at least five seconds between snapshot polls. The client example waits for at most two minutes. If still pending, retain the ID and any qualified partial result for a later user-requested check. Never resubmit because a lookup is slow. 6. Answer: present the returned dates, service names and council source with the relevant address, confidence, completeness and subscription qualifications. Save `property_id` only if the integration needs to retrieve the schedule later. An assistant needs a permitted HTTP tool or installed integration to execute this journey. Reading these pages does not itself install a tool. A GET-only fetcher can read documentation and existing resources; creating a lookup needs POST support. Pending work is not a promise of eventual success. ### Check that you can reach the API This fixed-reference request creates no lookup job: ```sh curl --fail-with-body \ https://whenisbins.com/v1/waste-types ``` A shortened response looks like this: ```json { "waste_types": [ { "id": "refuse", "display_name": "General waste" }, { "id": "recycling", "display_name": "Recycling" }, { "id": "garden", "display_name": "Garden waste" } ] } ``` Dates use ISO 8601 `YYYY-MM-DD` format; timestamps use ISO 8601 UTC format. Interpret relative dates such as "today" in `Europe/London`. ## The lookup journey A lookup normally takes these steps: 1. Ask `/addresses` what information the council needs for the postcode. 2. Send that information to `POST /lookups`. 3. If the lookup is still running, wait for it to change. 4. Read the schedule from the completed lookup. 5. Save `property_id` if you need to retrieve the address-free schedule later. Some lookups finish immediately from stored information. Others take longer because the service must check the council's website. Always inspect the returned `status`; do not assume a `201 Created` response already contains a finished schedule. ### 1. Find out what address information is needed Send the postcode to `GET /addresses`: ```sh curl --fail-with-body --get \ --data-urlencode "postcode=CB4 2HX" \ https://whenisbins.com/v1/addresses ``` The response identifies the collecting council and tells you what to send next. For a council with a property list it may look like this: ```json { "postcode": "CB4 2HX", "council": { "id": "E07000008", "name": "Cambridge City Council", "lookup_url": "https://www.cambridge.gov.uk/check-when-your-bin-will-be-emptied", "expected_wait_seconds": 8, "success_rate": 0.96 }, "required_input": "property_id", "postcode_representative": "opt_in", "candidates_source": "council", "candidates": [ { "id": "p:4c5ee6c2f2c7c959", "label": "15 EXAMPLE COURT, EXAMPLE ROAD, CAMBRIDGE, CB4 2HX" } ] } ``` The response is a guide for the next request: - `required_input` tells you which field or fields to ask the user for. - `candidates` contains council-provided properties when a pick-list is available. Display `label` and send the selected `id` as `property_id`. - `input_options`, when present, contains council-provided roads or areas. Display each option's `label` and send its `value` in the field named by `input_options.field`. - When `input_options.needs_more_query` is `true`, its `options` array is empty on purpose. Ask for more of the road or area name and repeat the request with a narrower `q`. - `input_options.not_listed_value` is the value for a final "none of these" choice. It returns the user to ordinary address entry and must never be sent to a council directly. - `candidates_source: "unavailable"` means the service does not currently hold a property list. Ask for the first line of the address and send it as `property`. - `postcode_representative` tells you whether a labelled neighbouring-property answer is `automatic`, available after explicit `opt_in`, or `unavailable`. - `expected_wait_seconds` is an estimate, not a deadline. It can be `null` when there is not enough recent evidence. - `success_rate` is the recent success rate from `0` to `1`. It can also be `null`. `required_input` can have the following values: | Value | What the client should send to `POST /lookups` | |---|---| | `none` | Only `postcode` is required. | | `property_id` | A selected `candidates[].id` as `property_id`. | | `property` | The first line of the address as `property`. | | `street` | A road or street name as `street`. | | `road_and_locality` | Both `street` and `locality`. | | `street_and_property` | Both `street` and `property`. This exists for compatibility with a small number of council journeys. | | `settlement` | A town, village, island or settlement as `locality`. | | `settlement_or_road` | The council-recognised settlement or road as `locality`. | | `normal_weekday` | One named weekday, such as `Monday`, as `normal_weekday`. | | `normal_weekday_and_locality` | Both `normal_weekday` and `locality`. | | `property_type` | One of the returned council journey's supported residential property types as `property_type`. | The `property_type` values currently accepted are `private_flat_without_bin_store`, `private_block_with_bin_store`, `housing_estate`, and `street_bag_collection`. Only ask for one when `required_input` is `property_type`. For road and area pickers, you can narrow `input_options` with the optional `q` parameter: ```sh curl --fail-with-body --get \ --data-urlencode "postcode=EH14 7AL" \ --data-urlencode "q=Lanark Road" \ https://whenisbins.com/v1/addresses ``` Do not guess or parse candidate IDs. Treat every `property_id` as an opaque string and send it back unchanged. #### When an exact address is not listed If `postcode_representative` is `opt_in` and the lookup returns `422 address_not_found`, you may offer a clearly worded choice to use dates from a comparable neighbouring property. Only after the user agrees, repeat the same lookup with: ```json { "postcode": "CB4 2HX", "property": "15", "allow_postcode_representative": true } ``` When the value is `automatic`, the service may return a representative result without this flag because recent evidence shows the postcode is homogeneous. When it is `unavailable`, do not offer the option. In every case, inspect the successful Schedule's `address_match`; representative answers remain labelled `postcode_representative` and include a caveat in `notes`. ### 2. Create the lookup Send JSON to `POST /lookups`. The simplest direct lookup contains a postcode and the first line of the address: ```sh curl --fail-with-body \ -X POST \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 33f146d8-7ff4-4d1c-979d-8a5cb0441dd1" \ -d '{"postcode":"CB4 2HX","property":"15"}' \ https://whenisbins.com/v1/lookups ``` If the user selected a candidate, send its ID instead: ```json { "postcode": "CB4 2HX", "property_id": "p:4c5ee6c2f2c7c959" } ``` Use the field or fields requested by `/addresses`. Do not send unrelated fields. Unknown fields are rejected. The optional `Idempotency-Key` header makes retries safe. Generate a new random value for one logical lookup and reuse that value only when retrying the same request. Reusing it with different lookup details returns `409 idempotency_conflict`. The API returns `201 Created` after it creates the durable lookup. The `Location` header contains its status URL. The response body is the same Lookup resource returned by `GET /lookups/{lookup_id}`. #### A lookup that finished immediately ```json { "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "status": "done", "created_at": "2026-09-07T09:00:00Z", "completed_at": "2026-09-07T09:00:01Z", "council": { "id": "E07000008", "name": "Cambridge City Council", "lookup_url": "https://www.cambridge.gov.uk/check-when-your-bin-will-be-emptied" }, "expected_wait_seconds": 8, "success_rate": 0.96, "result": { "property_id": "p:4c5ee6c2f2c7c959", "matched_address": "15 EXAMPLE COURT, EXAMPLE ROAD, CAMBRIDGE, CB4 2HX", "address_match": "exact", "council": { "id": "E07000008", "name": "Cambridge City Council", "lookup_url": "https://www.cambridge.gov.uk/check-when-your-bin-will-be-emptied" }, "collections": [ { "name": "Black bin", "waste_type": "refuse", "dates": ["2026-09-10", "2026-09-24"], "dates_complete": true, "bin_colour": "black", "lid_colour": "black", "colour_source": "council", "container": "wheelie_bin" }, { "name": "Blue bin", "waste_type": "recycling", "dates": ["2026-09-17", "2026-10-01"], "dates_complete": true, "bin_colour": "blue", "lid_colour": "blue", "colour_source": "council", "container": "wheelie_bin" } ], "date_confidence": "published_calendar", "date_completeness": "limited_horizon", "evidence_granularity": "property", "retrieved_at": "2026-09-07T09:00:01Z", "source_url": "https://www.cambridge.gov.uk/check-when-your-bin-will-be-emptied" } } ``` The example values are illustrative and the response is shortened. Collection names, dates, colours and source details always depend on what the relevant council publishes. #### A lookup that is still running ```json { "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "status": "queued", "created_at": "2026-09-07T09:00:00Z", "council": { "id": "E07000008", "name": "Cambridge City Council" }, "expected_wait_seconds": 120, "success_rate": 0.81, "progress": { "stage": "deterministic", "message": "Checking the council's fastest sources", "steps": [ { "stage": "deterministic", "state": "active" } ] } } ``` Keep the lookup `id` and continue with the wait endpoint. ### 3. Wait for a running lookup The preferred browser flow is long polling: ```sh curl --fail-with-body -i \ "https://whenisbins.com/v1/lookups/f47ac10b-58cc-4372-a567-0e02b2c3d479/wait" ``` The request waits until the lookup changes or for about 25 seconds. Read the `X-Lookup-Cursor` response header and send it as `after` when reconnecting: ```sh curl --fail-with-body -i --get \ --data-urlencode "after=CURSOR_FROM_THE_PREVIOUS_RESPONSE" \ "https://whenisbins.com/v1/lookups/f47ac10b-58cc-4372-a567-0e02b2c3d479/wait" ``` If a response supplies `Retry-After`, respect it before reconnecting. Only keep one active wait request for the same lookup and IP address. If long polling is inconvenient, fetch a single snapshot from `GET /lookups/{lookup_id}`. Send the response's `ETag` in `If-None-Match` on the next request; an unchanged lookup returns `304 Not Modified` with no JSON body. ### 4. Handle every lookup status | Status | Meaning | What the client should do | |---|---|---| | `queued` | The lookup is waiting to run. | Show the wait estimate and keep waiting. | | `running` | The council is being checked. | Show progress when present and keep waiting. | | `partial` | A usable result is present, but more complete dates may still arrive. | Display it honestly and keep waiting. Check every collection's `dates_complete`. | | `done` | The lookup has settled successfully. | Display `result`; no further polling is needed. | | `failed` | The lookup has settled without a successful result. | Display `detail`, offer `council.lookup_url` when present, and stop polling. | A `partial` result can be provisional. When `result.provisional` is `true`, it is a clearly labelled schedule from a neighbouring property while the exact property is still being checked. It always has `address_match: "postcode_representative"`. Do not cache it, send reminders from it, or treat it as final. Continue waiting for the exact result. `progress.steps` records only the lookup methods already reached. A step can be `active`, `waiting`, or `missed`. A missed step did not produce an answer; it is not a completed stage in a pipeline. `queue_ahead`, when present, is the number of citizen lookups waiting ahead of this one. ### 5. Retrieve the stable schedule later The result of a successful lookup contains `property_id`. Save that opaque value if your service needs to re-check the schedule later. In the URL only, drop its leading `p:` namespace marker; the remaining value is the opaque `property_token`: ```sh curl --fail-with-body -i \ "https://whenisbins.com/v1/schedules/4c5ee6c2f2c7c959" ``` This Schedule resource is deliberately address-free. It does not contain `matched_address`, because possession of a property ID is not authentication. Save its `ETag` and use a conditional request next time: ```sh curl --fail-with-body -i \ -H 'If-None-Match: "ETAG_FROM_THE_PREVIOUS_RESPONSE"' \ "https://whenisbins.com/v1/schedules/4c5ee6c2f2c7c959" ``` - `200 OK` contains the latest stored Schedule. - `304 Not Modified` means the schedule content has not changed and has no JSON body. - `404 no_schedule` means the service has never answered for that property. Create a lookup; do not interpret it as an empty collection calendar. An older schedule can still be returned while the service refreshes it. Read `retrieved_at` to see when the council information was fetched. An older schedule is not converted into a 404 merely because it is due for refresh. ## Presenting the answer to the user Name each collection service and give its next returned date, or the returned dates within the period the user asked about. Include the council source link and preserve the response's notes and qualifications. When useful, state when the council information was retrieved; `retrieved_at: null` means unknown, not the time of this request. For a question about the next collection, a useful answer has this shape: "Your next [service name] collection is [returned date]. Source: [council link]." Add the applicable caveat beside that date, such as "This next date is projected from the council's published weekday" or "These are provisional dates from a neighbouring property; your address is still being checked." These are templates, not real collection information. - Interpret "today", "tomorrow" and "this week" using `Europe/London` and the user's intended period. A collection date is a local calendar date, not a UTC collection-time timestamp. - Keep `date_confidence`, `date_completeness`, `dates_complete`, `address_match`, `evidence_granularity` and relevant `notes` meaningful in the answer. An exact address match does not necessarily mean a complete, property-level calendar. - Keep a provisional neighbouring-property answer labelled as provisional. Continue the exact lookup; never promote the provisional answer after failure or use it for reminders, feeds or a persistent schedule cache. - Mention `subscription_required` when present. Garden collection dates do not prove that the household subscribes. - Do not infer bin contents from colour, invent a collection time or put-out deadline, expand weekday wording into dates, or assume a bank-holiday shift. - Missing dates mean information is unavailable, not that no collection exists. On failure, explain `detail` and offer the council's lookup link when present. ## Understanding a schedule ### Address and evidence | Field | Meaning | |---|---| | `property_id` | The service's opaque identifier for the property. Do not parse it. | | `matched_address` | The council-rendered address. Present only inside a private Lookup result, not the stable Schedule. | | `address_match` | How the address was matched: `exact`, `street`, `postcode`, or the labelled neighbour result `postcode_representative`. | | `council` | The collecting council. `council.id` is its official GSS code. | | `evidence_granularity` | The level at which the council publishes the evidence: `property`, `street`, `postcode`, `area`, or `round`. | | `source_url` | The council page used for the lookup, or its known bin-checker page. | | `retrieved_at` | When the information was fetched from the council. `null` for historical cached lookups whose original retrieval time was not retained. | `address_match` and `evidence_granularity` answer different questions. For example, the service may match a property exactly and then use the council's verified round calendar. That result has `address_match: "exact"` and `evidence_granularity: "round"`. ### Collection entries Each entry in `collections` represents one council waste service. Do not merge entries just because they share a date; some councils genuinely provide many separate recycling services. | Field | Meaning | |---|---| | `name` | The service name exactly as the council presents it. | | `waste_type` | A standard category such as `refuse`, `recycling`, `garden` or `food`. Use `/waste-types` for the complete vocabulary. | | `dates` | Sorted upcoming collection dates. Clients may derive the weekday for display. | | `schedule` | Council-published weekday or time wording, when relevant. Clients must not expand it into further dates. | | `dates_complete` | False during a partial lookup, or when a historical dated answer has no remaining dates for this service. If the lookup is `done`, request a fresh lookup instead of polling the old one. | | `container` | `wheelie_bin`, `caddy`, `box`, `bag`, `sack`, or `communal`. | | `bin_colour` and `lid_colour` | Council-stated colours, or a neutral default. | | `colour_source` | `council` when the council stated the presentation details; otherwise `default`. | | `subscription_required` | Present and `true` when the service is a paid opt-in, or when a representative garden-waste result cannot prove this property's subscription. | ### Date trust labels The API deliberately separates confidence from completeness: | Field and value | Meaning | |---|---| | `date_confidence: published_calendar` | The council publishes concrete calendar dates. | | `date_confidence: council_projection` | The council, or a bounded service rule based on the council's published weekday, produced one next date. Holiday exceptions may be missing. | | `date_confidence: next_collection_only` | The source provides only the next collection date or dates. Do not imply a longer calendar exists. | | `date_completeness: next_only` | The answer contains concrete next dates only. | | `date_completeness: limited_horizon` | The answer is valid but covers less than the full serving window. | | `date_completeness: full_horizon` | The source was verified across the full serving window. | | `date_completeness: weekday_only` | The council published weekday wording that could not safely be converted into dates. Read `schedule`. | Never generate additional dates from a pattern yourself. The API returns concrete dates for up to 92 days ahead and labels the narrow cases where one next date was projected from a council-published weekday. ## Public schedule identifiers When public calendar feeds are enabled and the service has verified the identifier, a Schedule can also contain: - `uprn`: the property's verified Unique Property Reference Number. - `council_id`: the council's own identifier for that property. This is not the council's GSS code; the GSS code is `council.id`. These identifiers support two additional address-free Schedule endpoints: ```text GET /schedules/uprn/{uprn} GET /schedules/council/{gss}/{id} ``` They resolve only schedules the service already holds. A uniquely mapped stale schedule is returned immediately and may be refreshed in the bounded background queue. Unknown, invalid, unverified or ambiguous identifiers never create work. The routes return `404 no_schedule` when public feeds are not enabled, the identifier is invalid or unverified, no schedule exists, or the identifier does not select exactly one property. ## Free allowances | Activity | Anonymous | Free token | |---|---|---| | Ordinary data reads | 60/minute and 1,000/day per network | 300/minute and 10,000/day per token | | Address discovery requests | 10/minute and 60/hour | 60/minute and 600/hour | | Lookup submissions | 10/minute and 120/hour | 60/minute and 1,000/hour | | New lookup or refresh work | 10/hour and 25/rolling 24 hours | 100/hour and 500/rolling 24 hours | | Status reads | 2,400/hour, separate from ordinary reads | 2,400/hour per token | Request windows conservatively include the boundary minute. An IPv4 address or IPv6 /64 network shares the anonymous allowance. Shared networks can request a token; tokens have their own allowance across networks. An outer network traffic limit, a process-wide external traffic ceiling and global capacity controls always apply. A cache hit or joining an existing job does not consume new-work allowance. A new refresh does, even when triggered by a GET. Failed work still counts if it was admitted. Address discovery has separate provider budgets and concurrency limits; cached candidates remain useful when a live provider is unavailable. Documentation has no application data quota. On 429, wait for Retry-After seconds; the response explains which allowance was reached and gives token_request_url. On 503, capacity or a dependency is unavailable: wait for Retry-After. A token does not guarantee capacity or bypass the shared spending ceiling. Do not rotate IPs, tokens or submissions to avoid limits. Bulk use needs a discussion with the operator. ## Authentication and free tokens Occasional queries need no token. For regular use, request a free token by emailing hello@whenisbins.com with your intended use and approximate volume. You will receive a one-time delivery link, valid for 24 hours. Open it and press Reveal my API token when ready to save the token privately. Opening the link alone does not consume it. Treat the link like a temporary password. Store the token in WHENISBINS_API_TOKEN and send Authorization: Bearer on data requests. Do not put a token in a URL or public browser JavaScript. A supplied invalid or revoked token receives 401; it is never silently treated as an anonymous request. ## Errors An error response has a stable machine-readable code and a plain-English explanation: ```json { "problem": "address_not_found", "detail": "The council's system doesn't list that exact address." } ``` Depending on the error, it can also contain `council`, `council_note`, `candidates`, or `postcode_representative`. Display `detail` to the user and branch on `problem` in code. Do not branch on the wording of `detail`. | HTTP status | Problem | What it means or what to do | |---|---|---| | 400 | `invalid_postcode` | Send a complete UK postcode. | | 400 | `invalid_request` | Correct the malformed value, unsupported field or invalid idempotency key described in `detail`. | | 401 | `unauthorized` | A supplied token is invalid or revoked, or the service is in token-only mode. Ask for a replacement; do not silently discard a rejected credential. | | 403 | `invalid_request` | The browser origin is not allowed to create lookups. Server-to-server clients are not subject to the browser origin check. | | 404 | `postcode_outside_coverage` | The postcode is valid but does not resolve to a collecting council the service covers. | | 404 | `lookup_not_found` | The lookup ID is unknown or its 30-day retention period has passed. | | 404 | `no_schedule` | No stored Schedule exists for that identifier. | | 404 | `not_found` | The `/v1` route does not exist. | | 405 | `method_not_allowed` | Use the method named by the `Allow` response header. | | 409 | `idempotency_conflict` | The idempotency key was already used for different lookup details. | | 413 | `invalid_request` | The JSON body is too large. Send only the fields `/addresses` asked for. | | 415 | `invalid_request` | Send a JSON body with `Content-Type: application/json`. | | 422 | `council_not_supported` | Use `council.lookup_url` when present. The API cannot automate this council safely. | | 422 | `postcode_outside_coverage` | `POST /lookups` could not resolve the postcode to a collecting council the service covers. | | 422 | `ambiguous_address` | Ask for the missing council-specific input or let the user select a returned candidate. | | 422 | `address_not_found` | Check the first line of the address or let the user select a returned candidate. | | 422 | `property_identifier_required` | Add a house number or building name; a flat or floor description alone is insufficient. | | 422 | `council_site_maintenance` | The council's checker is showing its own maintenance page, so no lookup is started. Try again later. | | 429 | `rate_limited` | Wait for `Retry-After`; `limit_scope` and `token_request_url` may explain how to request more access. | | 500 | `server_error` | An unexpected service error occurred. It is safe to retry with the same idempotency key where applicable. | | 503 | `service_unavailable` | A required service is temporarily unavailable. Wait for `Retry-After` when supplied. | A lookup can also finish with `status: "failed"`. In that case its body uses the same `problem` and `detail` fields. Additional final lookup problems include: - `council_site_unavailable`: the council's checker could not be reached. - `council_site_maintenance`: the council is showing a temporary maintenance page. It is also returned immediately as the `422` above when a recent observation of that maintenance page is still current. - `property_dates_unavailable`: the council lists the property but currently publishes no collection dates for it. Re-entering the same address will not fix that gap. ## Caching, privacy and browser access - `GET /addresses`, `/schedules`, `/councils` and `/waste-types` are public, cacheable resources and allow cross-origin browser reads. - Lookup creation and lookup-status responses can contain an exact address. They use `Cache-Control: private, no-store` and browser access is restricted to approved WhenIsBins origins. Server-to-server callers can use them without an `Origin` header. - Never place a full address in a URL. Send property details only in the JSON body of `POST /lookups`. - A postcode and address together are personal data. Keep lookup IDs, property IDs and response bodies out of long-lived logs unless you have a documented need and retention policy. - Lookup resources are retained for 30 days. Stable address-free Schedule resources are separate from that lookup history. ## Complete JavaScript example This example uses Node 22+ and no npm packages. It separates discovery from the user's selection and saves the request and retry key before submitting. It uses the [published JavaScript client](https://whenisbins.com/v1/client.mjs). Work in a new private local directory for one lookup. Download the client as `client.mjs` and inspect it before running it: ```sh curl --fail-with-body --output client.mjs https://whenisbins.com/v1/client.mjs ``` Save the following as `lookup.mjs` in that directory. Tokens, if needed, come from `WHENISBINS_API_TOKEN` in the environment. JSON input goes through stdin; address details never go in command arguments. The state files and output are private: keep them out of source control, shared folders and long-lived logs, and delete them when the lookup and any requested continuation are finished. Run one command at a time in this directory. ```js import { readFile, writeFile } from "node:fs/promises"; import { randomUUID } from "node:crypto"; import { createWhenIsBinsClient } from "./client.mjs"; const api = createWhenIsBinsClient({ token: process.env.WHENISBINS_API_TOKEN }); const command = process.argv[2]; const readJson = async path => JSON.parse(await readFile(path, "utf8")); async function readInput() { process.stdin.setEncoding("utf8"); let text = ""; for await (const chunk of process.stdin) { text += chunk; if (Buffer.byteLength(text) > 16384) throw new Error("Input exceeds 16 KiB."); } return JSON.parse(text); } const saveNew = (path, value) => writeFile(path, JSON.stringify(value), { mode: 0o600, flag: "wx", // Refuse to replace another lookup's saved state. }); const pending = lookup => ["queued", "running", "partial"].includes(lookup.status); const print = value => console.log(JSON.stringify(value, null, 2)); try { if (command === "addresses") { const { postcode, q } = await readInput(); print(await api.addresses(postcode, q)); // No selection or submission here. } else if (command === "prepare") { const input = await readInput(); // Only fields requested by /addresses. await saveNew("request.json", { input, idempotencyKey: randomUUID() }); print({ prepared: true }); // The exact body and key now exist before POST. } else if (command === "submit") { // A saved receipt means the lookup already exists: use wait instead. let receipt; try { receipt = await readJson("submission.json"); } catch (error) { if (error.code !== "ENOENT") throw error; } if (receipt) { print(receipt); } else { const { input, idempotencyKey } = await readJson("request.json"); const response = await api.submit(input, { idempotencyKey }); receipt = { lookup: response.data, retryAt: Date.now() + response.retryAfterSeconds * 1000 }; await saveNew("submission.json", receipt); print(receipt); } } else if (command === "wait") { const receipt = await readJson("submission.json"); let previous = receipt.lookup; let retryAt = receipt.retryAt; try { const saved = await readJson("answer.json"); previous = saved.lookup; retryAt = saved.retryAt; } catch (error) { if (error.code !== "ENOENT") throw error; } if (!pending(previous)) { print({ lookup: previous, timedOut: false }); } else { const answer = await api.wait(previous.id, { maxWaitMs: 120000, retryAfterSeconds: Math.max(0, Math.ceil((retryAt - Date.now()) / 1000)), }); // A timeout placeholder must not erase an earlier qualified partial answer. if (answer.timedOut && pending(answer.lookup) && !answer.lookup.result && previous.status === "partial" && previous.result) answer.lookup = previous; answer.retryAt = Date.now() + (answer.retryAfterSeconds ?? 0) * 1000; await writeFile("answer.json", JSON.stringify(answer), { mode: 0o600 }); print(answer); // Includes all evidence fields; a failed lookup stays failed. } } else { throw new Error("Use addresses, prepare, submit or wait."); } } catch (error) { print({ error: "command_failed", status: error.status ?? null, problem: error.problem ?? null, retryAfterSeconds: error.retryAfterSeconds ?? null, detail: "No automatic retry was made. Check input and saved state. An uncertain submission must reuse request.json unchanged." }); process.exitCode = 1; } ``` First save the user's postcode in `postcode.json`, for example `{"postcode":"USER_POSTCODE"}`, replacing the placeholder. Run: ```sh node lookup.mjs addresses < postcode.json ``` Read the returned `required_input`. Show candidate labels to the user when a choice is needed. Save only the confirmed request fields in `selected.json`: for a property list, use the postcode and the exact selected `candidates[].id` as `property_id`; for other journeys follow the [required input table](#1-find-out-what-address-information-is-needed). Do not copy an illustrative ID from this guide or submit if the selection is ambiguous. For road/area searches, add `q` to `postcode.json` and repeat discovery until the user can choose a returned option. Discovery alone never submits work. After the user has supplied or confirmed the necessary details: ```sh node lookup.mjs prepare < selected.json node lookup.mjs submit ``` Inspect `lookup.status`. If it is `queued`, `running` or `partial`, run: ```sh node lookup.mjs wait ``` A timeout means this command stopped waiting, not that the lookup failed. Retain the qualified partial answer if present and offer a later check of the same ID. Run `wait` again only for a user-requested continuation. Terminal `done` and `failed` states cause no further polling. The example prints the full response so [answer qualifications](#presenting-the-answer-to-the-user) remain available; it creates no reminders or subscriptions. The client's timed-out wait includes `retryAfterSeconds` when a server delay remains. The example saves its expiry as `retryAt`, so a later continuation respects the remaining delay instead of either polling early or restarting the whole delay each time. If submission loses its response, keep `request.json` unchanged and explicitly rerun `submit` after addressing the error and any `Retry-After`. It reuses the same key and body. If a receipt was already saved, it returns that receipt without another POST. Do not run `prepare` again to recover an uncertain request. A corrected or different lookup needs a separate private directory and a new key. These commands call the production API when you run them. There is no public sandbox. Repository tests execute this exact JavaScript example against offline fixtures, including a lost submission response, ambiguous address choices, partial results, failures and rate limits; those tests create no live jobs. ## OpenClaw and other agent clients For an agent with permitted HTTP tools, start with the [agent quickstart](#agent-quickstart) and [Markdown guide](https://whenisbins.com/v1/guide.md). No package installation is required to call the HTTP API. A fresh lookup needs POST support; OpenClaw's GET-only web_fetch tool cannot create one. The [OpenClaw skill](https://whenisbins.com/v1/openclaw-skill.md) is a standalone instruction file, not a complete helper package. Review its contents before adding it to your agent's skills. With only that file, use permitted HTTP tools. The [JavaScript example](#complete-javascript-example) above is independently usable without a ClawHub account or unpublished package path. If you already have the complete reviewed WhenIsBins package, it also contains the client, guide and Node 22+ helper. Install that local package with `openclaw skills install /absolute/path/to/whenisbins`, substituting its actual location, then start a new OpenClaw conversation. From the package directory, run `node scripts/whenisbins.mjs check` for a fixed-reference request or `node scripts/whenisbins.mjs --help` for command inputs. The helper journey is `addresses`, user selection, `key`, `submit`, then `wait` while pending. Save the key and input before submitting, and pass the submission's `retryAfterSeconds` to `wait`. Preserve a partial result if waiting times out. For other agent clients, use the HTTP quickstart or JavaScript client with a permitted tool. An MCP server is not currently available. The same address, date, consent and retry rules apply whichever client makes the requests. ## Endpoint summary | Method and path | Purpose | |---|---| | `GET /addresses` | Resolve a postcode to its council and required address input. | | `POST /lookups` | Start or immediately satisfy a property lookup. | | `GET /lookups/{lookup_id}` | Fetch one lookup snapshot. | | `GET /lookups/{lookup_id}/wait` | Wait for a lookup to change. | | `GET /schedules/{property_token}` | Retrieve the stable address-free Schedule. | | `GET /schedules/uprn/{uprn}` | Retrieve an existing Schedule by verified UPRN. | | `GET /schedules/council/{gss}/{id}` | Retrieve an existing Schedule by a council property identifier. | | `GET /councils` | List collecting councils and coverage status. | | `GET /waste-types` | List the fixed waste-type vocabulary. | Documentation is available from the API itself: - `GET /openapi.yaml` returns the machine-readable OpenAPI 3.0 contract. - `GET /docs` renders that contract as browsable reference documentation. - `GET /guide` returns this guide as a web page. There are no bulk endpoints, webhooks or API-provided calendar files in v1. The public website may offer calendar feeds and reminders, but those are separate website features. ## Further reference - [OpenAPI contract](openapi.yaml) — exact request and response schemas. - [Browsable API reference](https://whenisbins.com/v1/docs) — the same contract as HTML. - [Markdown guide](https://whenisbins.com/v1/guide.md) — the same guide as plain text. For integration questions or higher allowances, email hello@whenisbins.com with your intended use and approximate volume.