UK bin collection dates for apps and AI agents
AI agent, or short on context? Read the one-page quickstart first: the whole happy path and recurring checks in one page. This guide is about 50 KB.
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.
For personal use, start without an account or API key, within the free anonymous limits. Commercial users must contact hello@whenisbins.com before using the API, including through MCP. For regular use, request a free token: its allowance is yours alone, not shared with everyone on your network. Read where our information comes from and how to use it. Using the API or MCP server means accepting our terms and conditions.
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.
- MCP server —
https://whenisbins.com/mcp, for AI clients that support remote MCP servers. - Agent quickstart — get from a postcode to an answer.
- Checking again on a schedule — weekly reminders and other recurring use.
- Missed-bin reporting links — the council's own page for reporting a missed collection.
- 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, and AI clients that support a remote MCP server.
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. It supplies the council's own missed-bin reporting link when one has been verified.
Missed-bin reporting links
WhenIsBins reads collection dates. It does not set reminders, check whether a bin was emptied or report a missed bin, and there is no WhenIsBins report-submission endpoint. Where we hold a verified link, a response names the council's own page for reporting a missed collection, which may be useful if you are helping someone whose bin was not emptied.
council.missed_bin_urlappears on a Lookup's council andresult.council, in address discovery, council discovery and saved Schedules. It is the council's operational missed-collection page, notlookup_url(the date checker),source_url, or a formal complaint route.missed_bin_checked_atis the date that link was last verified in our registry, not a live availability check or the schedule's retrieval time.- Both fields are null when no approved verified link is held. Older responses may omit them; treat omission like null. Approved links remain visible until deliberately updated or withdrawn, even after their review date.
Reporting rules differ by council and waste stream: when reports open and close, known disruptions, exclusions, the evidence needed and whether the crew returns. The council's page is the authority on all of them. For example, checked on 2 October 2026:
- Birmingham: general waste reports open after 1:30pm; known incomplete rounds are excluded.
- Colchester: reports open after 5:30pm and must be made within 2 days of the missed collection.
- Cambridge: reports open at 3:30pm and close at 3:30pm the next weekday (Monday to Friday). Cambridge does not return for missed outdoor food waste caddies.
A returned collection date is not proof that a collection was due for a particular household or that a report is eligible. Past dates drop out of a Schedule once the day has passed; this is not cancellation. Projected-date, representative-address, subscription and provisional qualifications apply to a date whatever it is used for.
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. The list is the council's own and normally at most 14 days old; when nothing fresher can be had, a copy up to 45 days old is returned rather than nothing.unavailable_candidates, when present, names properties the council lists but returned no collection record for on a recent lookup. Tell the user the truth about them instead of leaving them to conclude the address is missing, but never offer them as a choice: selecting or typing one is answered at once with422 property_dates_unavailable.source_notice, when present, says what the service already knows about the council's own checker, as aproblemand adetailsentence to show beside the address step. It is a notice on a successful response, never a refusal. Forcouncil_site_maintenanceandcouncil_not_supported, a fresh lookup would be refused with that problem, but a schedule the service already holds is still returned, so submit the lookup as normal. Forcouncil_site_unavailablenothing is refused: the council's website is proving hard to read, so the lookup may be slow and may fail.input_options, when present, contains council-provided roads or areas. Display each option'slabeland send itsvaluein the field named byinput_options.field. Wheninput_optionsis absent, supply the field requested byrequired_inputas free text. A street directory with no matches is omitted, sorequired_input: streetstill accepts a street name. The exception is a council whose whole road list is held (City of Edinburgh, and Nuneaton and Bedworth): aqmatching nothing returnsinput_optionswithtotal: 0and emptyoptions, which means the council does not list that road. Free text still works there.input_options.not_listed_help, when present, is the council's own help for a road that is not in its list (for Edinburgh, homes that use communal bins). Show itstextand link itslink_textto itsurl.- 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_sourceiscouncilwhencandidatescame from the council's own checker.unavailablemeans 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. A retry returns the same lookup id whether the first request was answered at once from stored information or is still running. A key is remembered for 30 days, or until its lookup is removed.
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.- A 304 means stored content is unchanged, not that the council re-verified the date.
304 Not Modifiedhas 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. The body'snext_stepsays so, withcreate_lookupandwebsitelinks: creating a lookup needs a POST, so a client that can only GET should use the website.
An older schedule can still be returned while the service refreshes it. Read retrieved_at to see when the council information was fetched and refresh_due_at for when the service considers it due for refresh: before that instant it is an acceptable cache, after it the read you just made has queued the refresh. An older schedule is not converted into a 404 merely because it is due for refresh.
Checking again on a schedule
A weekly reminder, a daily "what goes out tomorrow?" check and a home automation all follow the same pattern. Create one lookup, then read the stored Schedule. Do not create a new lookup on every run.
- Once: complete the lookup journey and save
result.property_idwith the user's agreement. Save the request body you sent as well, in case step 3 is ever needed. - Every run: send
GET /schedules/{property_token}with the lastETaginIf-None-Match. A 304 means stored content is unchanged, not that the council re-verified the date. A200updates the upcoming schedule. A missing past date is not cancellation. This read creates no lookup and does not use the new-work allowance unless it starts a background refresh. - Only when needed: repeat the lookup journey with the saved request body and a new
Idempotency-Key. That is needed after404 no_schedule, when a service the user relies on has nodatesleft, or when503persists after itsRetry-After. Save theproperty_idfrom the new result.
A property_id is derived from the council and the address as the council lists it. It stays the same between lookups of the same property and does not expire, so it is safe to store. It can change if the council changes how it lists the address or is reorganised into a new authority. Handle that through step 3 rather than assuming the identifier is permanent.
The service refreshes a stored Schedule in the background when a read finds it due, so an integration that reads weekly does not need its own refresh routine. The rule: a stored answer is fresh for 28 days after retrieved_at, or 7 days when today is within a fortnight of a UK bank holiday; some councils' own projected calendars are rechecked sooner. Once it is due, a read returns the stored answer as it stands and queues the refresh; a later read returns the refreshed answer with a new ETag and retrieved_at. An answer still unrefreshed 45 days after retrieved_at (28 near a bank holiday) is refused with 503 and Retry-After rather than served. Compare retrieved_at with today before relying on an old answer near a bank holiday.
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.
- The response does not currently mark a date the council has moved. A moved collection appears as a returned date on a different weekday from that service's other dates. You may tell the user that the date falls on a different day from usual. Give a reason only when
notesstates one, and never move or add a date yourself. - Do not work out weekdays by reasoning.
by_dategives each returned date itsweekdayand the services due. To answer "what goes out tomorrow?", look for tomorrow'sEurope/Londondate there. - 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. |
refresh_due_at | When this answer stops being fresh: retrieved_at plus 28 days, or plus 7 days within a fortnight of a UK bank holiday. Before it, no refresh is owed; after it, a read of the Schedule resource queues one and still returns the stored answer. Policy, not a promise of when the refresh runs. null when retrieved_at is. |
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. Services in one Schedule can carry different numbers of dates: a council may publish a calendar for refuse and recycling but only the next date for garden or food waste. |
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 whenever this service has no dates (including weekday_only answers). If the lookup is done, request a fresh lookup instead of polling the old one. |
container | wheelie_bin, caddy, box, bag, sack, or communal. A legacy field: wheelie_bin is also the default when no shape is known. |
bin_colour and lid_colour | Council-stated colours, or a neutral default. Legacy fields; prefer appearance_occurrences. |
colour_source | council when the council stated at least one of the two colours; otherwise default. |
bin_colour_source and lid_colour_source | The same flag for each colour separately. A lid colour inherited from the body is default. Check these before describing a body or lid colour as the council's. |
appearance | One conservative description of the container across every returned date: its kind, whether it has a lid, and separate overall, body and lid colour facts with their sources. Anything not evidenced is unknown or absent, never guessed. |
appearance_occurrences | The preferred container description, one row per returned date. Use the row whose date matches the collection you are describing and never borrow another date's row. Each row's variants are alternatives ("or"), and the items inside one variant go out together. A status of unknown means no description is held, not that there is no container. |
contents | What goes in the containers, summarised from council-published material labels. accepted lists those labels once; source is council. If null, say the council has not published contents in the evidence we hold; never guess from colour or name. varies_by_date: true means the materials, their container assignments, conditions or evidence coverage differ across dates or variants. |
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. |
contents.accepted combines labels across a collection's containers and alternatives. It is not an exhaustive list and does not mean every container takes every listed material. For which box or bag takes what, read the matching appearance_occurrences row and its variants[].items[].material, keeping the variant's condition visible. Those items remain authoritative, especially when varies_by_date is true. A false value does not establish completeness. Labels retain the council's wording and any qualifications, with HTML removed and whitespace tidied. The evidence currently has no separate exclusions list, so there is no not_accepted field.
Collections by date
by_date holds the same dates the other way round: one entry per day on which anything is collected, soonest first, with the weekday already worked out.
{
"by_date": [
{
"date": "2026-09-22",
"weekday": "Tuesday",
"collections": [
{ "name": "Black bin", "waste_type": "refuse" },
{ "name": "Garden waste", "waste_type": "garden", "subscription_required": true }
]
}
]
}
To answer "what goes out tomorrow?", find tomorrow's Europe/London date in by_date; no entry means nothing is due that day in the returned dates. It is a view over collections, not a second source: it adds no date, moves none and projects none, and the same date_confidence, date_completeness and dates_complete apply. weekday is only the weekday of that date. It does not say which day is usual, and a moved collection is not labelled. by_date is empty when no service has a concrete date.
Compact view
Add ?view=compact to any GET /schedules/... request for the same answer without the container descriptions. Each entry in collections keeps only name, waste_type, dates, dates_complete and, when present, schedule and subscription_required; by_date, the evidence and date-trust fields, notes and source_url are unchanged, and view: "compact" names the view. A full Schedule is mostly appearance evidence (about 9 KB against about 1 KB), so use the compact view for a reminder or a "what goes out tomorrow?" check. It carries no colour or container information at all, so nothing in it can be read as the council's description of a bin; request the full Schedule when you need to say which bin to put out. The compact body has its own ETag: send back the one you received for the same view. Any other view value is 400 invalid_request.
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.calendar_url: the public calendar feed those identifiers key; see "Subscribable calendar links" below.
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.
Subscribable calendar links
When a Schedule has either identifier it also has calendar_url: the property's public iCalendar feed on the website.
{
"uprn": "100023336956",
"calendar_url": "https://whenisbins.com/<UPRN>.ics"
}
Use the value as given; do not assemble a link yourself. It is the uprn form whenever the Schedule has a uprn, and otherwise https://whenisbins.com/{council.id}/{council_id}.ics. If the service later verifies that property's UPRN, the council form answers 301 to the UPRN form. A Schedule without calendar_url has no public calendar: never construct one from the opaque property_id, a postcode or an address. A completed Lookup's result carries the same field.
The feed is for a person to subscribe to in a calendar application, so give the user the link instead of fetching it on their behalf. It is not a /v1 route. It carries collection dates, service names and, where held, the council's description of each container: no address, postcode or personal details. By default it has one event per collection date naming every service due; add ?per=service for one event per service per date, and ?download=1 for a one-off file instead of a subscription. It sends an ETag and Cache-Control: public, max-age=3600, and honours If-None-Match.
It follows the same rules as the two endpoints above: it serves only a schedule the service already holds, never creates a lookup, and shares the ordinary data-read allowance. It answers 404, which is not an empty calendar, when the held schedule has no concrete dates (a provisional or weekday_only result). The dates are the Schedule's dates; read date_confidence and date_completeness from the API before telling a user how far ahead the calendar reaches.
Get only a calendar URL
Use the standard postcode lookup and address selection, then return only the subscription link:
- Call
GET /addresses?postcode=…and display the returnedcandidates[].label. - Let the user select their address. Send that candidate's
idunchanged asproperty_idtoGET /calendar-url.
curl --fail --silent --show-error --get \
--data-urlencode "postcode=CB4 2HX" \
"https://whenisbins.com/v1/addresses"
After the user selects an address, substitute its returned candidate ID:
curl --fail --silent --show-error --get \
--data-urlencode "property_id=p:0123456789abcdef" \
"https://whenisbins.com/v1/calendar-url"
No intervening POST /lookups, schedule download, UPRN or council identifier is needed. The selected address must already have a usable saved schedule. Never automatically select the first candidate. If the postcode response requires typed or council-specific input instead of offering property_id choices, use the ordinary lookup flow described above.
Callers that already know a confirmed identifier may skip the postcode step. The URL endpoint accepts exactly one of property_id (or its URL token), uprn (verified canonical digits), or gss and council_id together.
A successful response is 200, with Content-Type: text/plain and only https://whenisbins.com/100023336956.ics in the body. Save that URL and give it to the user to subscribe. The endpoint does not return a schedule or fetch the calendar file. It never starts a lookup or refresh, even on a miss.
The property must map uniquely to a public calendar and have a usable saved schedule with current dates. The URL is the same calendar_url a completed lookup or saved Schedule names for that property, and it is refused when it would not resolve uniquely. Unavailable, ambiguous, provisional, undated, expired or postcode-representative answers return 404 no_schedule. A store outage returns 503 service_unavailable. Invalid, repeated or extra query parameters return 400 invalid_request; do not send a written address. Errors use the ordinary JSON Problem format. Public feeds must be enabled. The link alone makes no promise about how far ahead its dates extend.
This endpoint has a separate 100 requests per rolling 24 hours allowance per anonymous network or per bearer token. The preceding /addresses call uses the standard address-discovery allowance, not this URL allowance. Successful and unsuccessful reads, including HEAD, count; they do not consume ordinary read allowance. A quota refusal returns 429, limit_scope: calendar_url and Retry-After. Outer traffic protections still apply. These request counters are process-local and reset on restart; the boundary minute is conservatively retained. Responses use Cache-Control: no-store for this initial small allowance. The subscription URL can be retained; fetching its .ics content continues to use the existing calendar read limits and bounded refresh behaviour.
Free allowances
| Activity | Anonymous HTTP | Tokenless MCP | Free token (HTTP or MCP) | Elevated token |
|---|---|---|---|---|
| Calendar URL only (HTTP) | 100/rolling 24 hours per network | Not exposed as an MCP tool | 100/rolling 24 hours per token | 100/rolling 24 hours per token |
| Ordinary data reads | 300/minute and 10,000/day per network | 300/minute and 10,000/day per network | 300/minute and 10,000/day per token | 600/minute and 50,000/day per token |
| Address discovery requests | 60/minute and 1,200/hour | 60/minute and 1,200/hour | 60/minute and 1,200/hour | 300/minute and 5,000/hour |
| Lookup submissions | 60/minute and 1,200/hour | 60/minute and 1,200/hour | 60/minute and 1,200/hour | 300/minute and 5,000/hour |
| New lookup or refresh work | 1000/hour and 5000/rolling 24 hours | 1000/hour and 5000/rolling 24 hours | 1000/hour and 5000/rolling 24 hours | 1000/hour and 5000/rolling 24 hours |
| Status reads | 6,000/hour, separate from ordinary reads | 6,000/hour per network | 6,000/hour per token | 6,000/hour per token |
Anonymous HTTP, tokenless MCP and free tokens have the same figures, set high so that AI agents are not refused by their network allowance. The difference is whose allowance it is: anonymous and tokenless MCP callers share theirs with everyone else reaching us from the same network, which for a cloud AI client or a proxy exit can be many users, while a token has its own. Sending a token selects the token allowance on either surface. An elevated token is an ordinary token the operator has raised for a named integration, with higher request and address-discovery figures; ask at hello@whenisbins.com if the token allowance is not enough for your regular use. Every allowance still sits under the shared capacity, provider and spend gates.
Status long-polling defaults to 4 simultaneous status waits per source IP, one wait per lookup per IP, and 50 across the web process. This is not a fixed per-token limit on active lookups. A second wait above these bounds receives 429 with Retry-After; close or finish an existing wait before opening another. MCP polls ordinary status reads and returns still_working when its own bounded wait ends. The MCP process holds at most 20 calls for these longer waits; further calls use a short wait and may return still_working so the client can check later. Queue health, queue depth and age, provider capacity and shared spend can separately refuse new work with 503 service_unavailable. Serialise or reduce parallel submissions, honour Retry-After, and continue checking an existing lookup instead of repeatedly creating one. A token does not reserve four or five worker slots.
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
Commercial users must contact hello@whenisbins.com before using the API, including through MCP.
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, unavailable_candidates or postcode_representative, and a 429 can contain limit_scope and token_request_url. 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: it has no online checker the service can read, or its checker is behind a CAPTCHA, which the service never bypasses. detail says which. |
| 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 | property_dates_unavailable | The council lists the property but currently has no collection record for it. Returned at once when the user selects or types a property named in unavailable_candidates. Re-entering the address will not help; offer council.lookup_url. |
| 422 | council_site_maintenance | The council's checker is showing its own maintenance page, so no fresh lookup is started. A schedule the service already holds is still returned. 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. This includes a postcode that could not be resolved because the postcode service did not answer: that is a 503 to retry, never postcode_outside_coverage. |
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 read, or the service had to stop trying. A lookup always ends: one that cannot be answered is failed with this problem instead of waiting indefinitely.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. It is also returned immediately as the422above for a property already named inunavailable_candidates.
A lookup that failed with address_not_found can carry candidates: the council's own list for that postcode. Let the user pick from it and submit a new lookup with the selected property_id.
Caching, privacy and browser access
GET /addresses,/schedules,/councilsand/waste-typesare cacheable resources and allow cross-origin browser reads. An anonymous response isCache-Control: public; a response to a request that supplied a token isprivate, so it is never stored in a shared cache.- 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.
MCP server
The four tools carry the same council.missed_bin_url and missed_bin_checked_at fields as HTTP (inside result.council for lookup results); see missed-bin reporting links.
If your AI client supports remote MCP servers, add this address instead of calling the HTTP API yourself (the one-page MCP quickstart is the short form of this section):
https://whenisbins.com/mcp
It uses the Streamable HTTP transport. Personal use needs no account, key or sign-in within the free allowances. Commercial users must contact hello@whenisbins.com before using the API, including through MCP. For claude.ai, follow Claude's custom connector setup instructions. For Claude Code, its documented HTTP configuration is claude mcp add --transport http whenisbins https://whenisbins.com/mcp. A client that can send request headers may add Authorization: Bearer <token> to use a free token's allowances. There is no OAuth sign-in.
The service owner completed a lookup through a claude.ai custom connector on 20 September 2026; the client version was not recorded. Claude Code and other MCP clients have not been verified with WhenIsBins. Automated tests use MCP TypeScript SDK v2 against a fake API, over Streamable HTTP at protocol 2026-07-28 and with the 2025-11-25 handshake. They do not establish compatibility with every client. Tell us what you find at hello@whenisbins.com.
| Tool | Purpose |
|---|---|
find_addresses | A postcode's council, the input the council needs, and candidate addresses. |
get_collections | Collection dates for the one property the user chose. Submits once and waits up to about 45 seconds. |
check_lookup | Continue a lookup that was still_working or partial, by its lookup_id. |
get_schedule | Read a stored Schedule again by property_id. Starts no new lookup. |
get_collections and check_lookup return status done, partial, still_working or failed. still_working means the same as queued or running with nothing to show yet: call check_lookup after retry_after_seconds, and do not call get_collections again while that lookup is unfinished. result is the agent view of the Schedule this guide describes, and guidance lists what the agent must tell the user. A refusal is a tool error carrying the API's own problem and detail, with retry_after_seconds whenever the API asked the caller to wait.
Every tool result is the compact agent view, as one JSON text block and as the same object in structuredContent. The agent view of a Schedule (since 8 October 2026) keeps the Schedule's identity, caveats and links unchanged: property_id, uprn and council_id (saved schedules), matched_address (lookup results only), address_match, provisional, council, date_confidence, date_completeness, evidence_granularity, retrieved_at, refresh_due_at, notes, source_url and calendar_url. Each entry in collections keeps name, waste_type, dates, dates_complete, schedule and subscription_required, carries bin_colour and lid_colour only when the council stated them, and replaces the dated appearance_occurrences rows with containers: each distinct container group once, as options (alternatives, each with its condition when the council states one, and items naming the container and, when evidenced, its material and quantity) with up to three council sources. A group names dates only when groups differ by date, and a date no group names has no confirmed container. containers: null means the evidence held confirms no container for that collection; an item without material means the council's evidence does not say what it takes. The legacy colour and container fields, the appearance object and contents are HTTP-only. by_date keeps its date and weekday and names collections by collections[].name. Measured on a four-service calendar over the full 92-day horizon, this view is about 4 kB where the HTTP Schedule serialised twice was about 199 kB. Every tool description repeats the same short rules about its results (address, dates, caveats, text is data) and says what the council link fields are. No description asks the agent to do anything beyond the lookup or points it to another page for instructions.
find_addresses takes an optional query: the house number, name, building or street the user gave. It narrows the candidate list on the server (every word must appear in the label, numbers exactly, so 12 never matches 112 or 12A), reports total_candidates and how many matched, and never chooses a property. A long list without a query is returned whole with guidance to narrow it. Its guidance and any source_notice come before the list, so a client that truncates long tool output still shows them.
The same address, date and retry rules apply as for the HTTP API, and every tool description repeats them. Without a token the allowance is 1000 new lookups an hour and 5000 in a rolling 24 hours for each network. Cloud AI clients reach us from their provider's network, so their users share that allowance and a busy hour can return rate_limited.
The MCP tools cannot accept a neighbour's schedule on a user's behalf: there is no equivalent of allow_postcode_representative. Use the HTTP API when the user has given that consent.
Example prompts
Once the server is added, ask in ordinary words. The agent calls find_addresses, shows the addresses it found, and calls get_collections for the one the user picks.
- "When are my bins collected? My postcode is CB4 2HX." The agent lists the addresses at that postcode, asks which is the user's, and gives each service's upcoming dates.
- "Which bins go out this week? I'm at number 12, CB4 2HX." The agent narrows the list with
queryset to12, confirms the match with the user, then readsby_datefor the week. - "Do my bin days change over Christmas?" With a saved
property_id, the agent callsget_scheduleand lists the dates the council published around the holidays, under the council's own name for each bin; it says so when the holiday period is beyond the dates held. - "My green bin wasn't emptied yesterday. Where do I report it?" The agent gives the council's own
missed_bin_urlwhen one is held, or the council's website when it is not. WhenIsBins does not report it.
Troubleshooting
- **The server will not connect.** Use exactly
https://whenisbins.com/mcp, with no sign-in or key. It speaks Streamable HTTP only. - **
rate_limited.** Wait theretry_after_secondsit gives. Users of a cloud AI client share its network's allowance, so a busy hour can refuse even a first request. - **
still_working.** Some councils' websites are slow. Callcheck_lookupwith thelookup_idafterretry_after_seconds; callingget_collectionsagain for the same property does not make it faster. - **The address is not in the list.** The list is the council's own. Check the postcode, then use the council's checker at
council.lookup_url. - **
council_site_unavailableorcouncil_site_maintenance.** The council's own website is not answering. Try again later or use the council's page. - **
council_not_supportedorpostcode_outside_coverage.** WhenIsBins cannot read that council's checker, or the postcode does not resolve to a council it covers. Council coverage lists every council. - **Anything else.** Email hello@whenisbins.com with the time, the tool and the problem code. Do not send your address unless we ask.
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 MCP server if the client supports one, or the HTTP quickstart or JavaScript client with a permitted tool. 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 /calendar-url | Return only a known property's subscription URL, without starting work. |
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 calendar files under /v1. The website's public calendar feed is named by a Schedule's calendar_url; see "Subscribable calendar links". Email reminders are a website feature with no API.
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 a raised allowance, 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.