WhenIsBinswhenisbins
AI ExemplarLearn more about how Public Digital uses AI to reimagine public services

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. 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.

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 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 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:

curl --fail-with-body \
  https://whenisbins.com/v1/waste-types

A shortened response looks like this:

{
  "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:

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:

{
  "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:

ValueWhat the client should send to POST /lookups
noneOnly postcode is required.
property_idA selected candidates[].id as property_id.
propertyThe first line of the address as property.
streetA road or street name as street.
road_and_localityBoth street and locality.
street_and_propertyBoth street and property. This exists for compatibility with a small number of council journeys.
settlementA town, village, island or settlement as locality.
settlement_or_roadThe council-recognised settlement or road as locality.
normal_weekdayOne named weekday, such as Monday, as normal_weekday.
normal_weekday_and_localityBoth normal_weekday and locality.
property_typeOne 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:

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:

{
  "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:

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:

{
  "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

{
  "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

{
  "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:

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:

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

StatusMeaningWhat the client should do
queuedThe lookup is waiting to run.Show the wait estimate and keep waiting.
runningThe council is being checked.Show progress when present and keep waiting.
partialA usable result is present, but more complete dates may still arrive.Display it honestly and keep waiting. Check every collection's dates_complete.
doneThe lookup has settled successfully.Display result; no further polling is needed.
failedThe 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:

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:

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

FieldMeaning
property_idThe service's opaque identifier for the property. Do not parse it.
matched_addressThe council-rendered address. Present only inside a private Lookup result, not the stable Schedule.
address_matchHow the address was matched: exact, street, postcode, or the labelled neighbour result postcode_representative.
councilThe collecting council. council.id is its official GSS code.
evidence_granularityThe level at which the council publishes the evidence: property, street, postcode, area, or round.
source_urlThe council page used for the lookup, or its known bin-checker page.
retrieved_atWhen 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.

FieldMeaning
nameThe service name exactly as the council presents it.
waste_typeA standard category such as refuse, recycling, garden or food. Use /waste-types for the complete vocabulary.
datesSorted upcoming collection dates. Clients may derive the weekday for display.
scheduleCouncil-published weekday or time wording, when relevant. Clients must not expand it into further dates.
dates_completeFalse 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.
containerwheelie_bin, caddy, box, bag, sack, or communal.
bin_colour and lid_colourCouncil-stated colours, or a neutral default.
colour_sourcecouncil when the council stated the presentation details; otherwise default.
subscription_requiredPresent 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 valueMeaning
date_confidence: published_calendarThe council publishes concrete calendar dates.
date_confidence: council_projectionThe 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_onlyThe source provides only the next collection date or dates. Do not imply a longer calendar exists.
date_completeness: next_onlyThe answer contains concrete next dates only.
date_completeness: limited_horizonThe answer is valid but covers less than the full serving window.
date_completeness: full_horizonThe source was verified across the full serving window.
date_completeness: weekday_onlyThe 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:

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

ActivityAnonymousFree token
Ordinary data reads60/minute and 1,000/day per network300/minute and 10,000/day per token
Address discovery requests10/minute and 60/hour60/minute and 600/hour
Lookup submissions10/minute and 120/hour60/minute and 1,000/hour
New lookup or refresh work10/hour and 25/rolling 24 hours100/hour and 500/rolling 24 hours
Status reads2,400/hour, separate from ordinary reads2,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:

{
  "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 statusProblemWhat it means or what to do
400invalid_postcodeSend a complete UK postcode.
400invalid_requestCorrect the malformed value, unsupported field or invalid idempotency key described in detail.
401unauthorizedA 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.
403invalid_requestThe browser origin is not allowed to create lookups. Server-to-server clients are not subject to the browser origin check.
404postcode_outside_coverageThe postcode is valid but does not resolve to a collecting council the service covers.
404lookup_not_foundThe lookup ID is unknown or its 30-day retention period has passed.
404no_scheduleNo stored Schedule exists for that identifier.
404not_foundThe /v1 route does not exist.
405method_not_allowedUse the method named by the Allow response header.
409idempotency_conflictThe idempotency key was already used for different lookup details.
413invalid_requestThe JSON body is too large. Send only the fields /addresses asked for.
415invalid_requestSend a JSON body with Content-Type: application/json.
422council_not_supportedUse council.lookup_url when present. The API cannot automate this council safely.
422postcode_outside_coveragePOST /lookups could not resolve the postcode to a collecting council the service covers.
422ambiguous_addressAsk for the missing council-specific input or let the user select a returned candidate.
422address_not_foundCheck the first line of the address or let the user select a returned candidate.
422property_identifier_requiredAdd a house number or building name; a flat or floor description alone is insufficient.
422council_site_maintenanceThe council's checker is showing its own maintenance page, so no lookup is started. Try again later.
429rate_limitedWait for Retry-After; limit_scope and token_request_url may explain how to request more access.
500server_errorAn unexpected service error occurred. It is safe to retry with the same idempotency key where applicable.
503service_unavailableA 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.

Work in a new private local directory for one lookup. Download the client as client.mjs and inspect it before running it:

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.

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:

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. 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:

node lookup.mjs prepare < selected.json
node lookup.mjs submit

Inspect lookup.status. If it is queued, running or partial, run:

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 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 and Markdown guide. 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 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 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 pathPurpose
GET /addressesResolve a postcode to its council and required address input.
POST /lookupsStart or immediately satisfy a property lookup.
GET /lookups/{lookup_id}Fetch one lookup snapshot.
GET /lookups/{lookup_id}/waitWait 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 /councilsList collecting councils and coverage status.
GET /waste-typesList 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

For integration questions or higher allowances, email hello@whenisbins.com with your intended use and approximate volume.


For exact request and response schemas, use the browsable API reference, or fetch the OpenAPI contract directly.