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.
- Agent quickstart — get from a postcode to an answer.
- Complete JavaScript example — discover, select, submit and wait.
- Markdown guide — this same guide for HTTP tools.
- OpenAPI contract — exact request and response schemas.
- Browsable reference — explore the contract.
- JavaScript client — dependency-free Node 22+ client.
- OpenClaw skill — 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 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.
- Discover: call
GET /addresses?postcode=...with a URL-encoded postcode. Readrequired_input,candidatesandinput_options. - Resolve: use the user's exact, unambiguous address or ask them to select a candidate. Follow
required_inputfor other council journeys. Never guess a property or consent to a neighbouring-property answer on the user's behalf. - Submit once: save the selected JSON and a new
Idempotency-Keyprivately, then send them toPOST /lookups. Reuse both unchanged after a lost response. - Inspect: HTTP
201means the Lookup was created, not necessarily completed.doneandfailedare terminal.queued,runningandpartialneed continuation. - Continue: poll the same lookup ID, respecting
Retry-Afterand 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. - Answer: present the returned dates, service names and council source with the relevant address, confidence, completeness and subscription qualifications. Save
property_idonly 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:
- Ask
/addresseswhat information the council needs for the postcode. - Send that information to
POST /lookups. - If the lookup is still running, wait for it to change.
- Read the schedule from the completed lookup.
- Save
property_idif 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_inputtells you which field or fields to ask the user for.candidatescontains council-provided properties when a pick-list is available. Displaylabeland send the selectedidasproperty_id.input_options, when present, contains council-provided roads or areas. Display each option'slabeland send itsvaluein the field named byinput_options.field.- When
input_options.needs_more_queryistrue, itsoptionsarray is empty on purpose. Ask for more of the road or area name and repeat the request with a narrowerq. input_options.not_listed_valueis 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 asproperty.postcode_representativetells you whether a labelled neighbouring-property answer isautomatic, available after explicitopt_in, orunavailable.expected_wait_secondsis an estimate, not a deadline. It can benullwhen there is not enough recent evidence.success_rateis the recent success rate from0to1. It can also benull.
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:
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
| 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:
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 OKcontains the latest stored Schedule.304 Not Modifiedmeans the schedule content has not changed and has no JSON body.404 no_schedulemeans 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/Londonand 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_granularityand relevantnotesmeaningful 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_requiredwhen 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
detailand 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 iscouncil.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
| 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:
{
"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 the422above 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,/councilsand/waste-typesare 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-storeand browser access is restricted to approved WhenIsBins origins. Server-to-server callers can use them without anOriginheader. - 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 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.yamlreturns the machine-readable OpenAPI 3.0 contract.GET /docsrenders that contract as browsable reference documentation.GET /guidereturns 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 — exact request and response schemas.
- Browsable API reference — the same contract as HTML.
- Markdown guide — the same guide as plain text.
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.