Partner API
By requestRead access to the Schemes.sg catalogue, so partner organisations can surface the right scheme inside their own product.
https://asia-southeast1-schemessg.cloudfunctions.net/partner_apiGetting access
This is not a self-serve API. We work with a small number of partner organisations, and each key is issued by hand so we know who is using the data and can reach them if something changes.
Quick start
One request, to check your key works and see the shape of a scheme.
Authentication
Every request carries your key in a header. There is no token exchange, no OAuth dance, and no expiry. Send it as X-API-Key.
Base URL and versioning
The version is a path segment, so a future version can ship alongside this one without changing any URL you have already built against.
There is one version today. A request with no version, or an unknown one, returns 404 rather than quietly falling back to v1.
Operations
Three read operations. One key covers all of them.
List schemes
GET/v1/schemes
Page through active schemes. Filters combine (a scheme must match each one), and repeating a filter matches any of its values: ?category=Education&category=Financial%20Assistance&area=BEDOK. Results exclude retired and inactive schemes, except with updated_since.
Query parameters
categorystringoptional- One of: Financial Assistance, Family & Children, Health & Wellbeing, Housing & Food, Education, Employment & Training, Seniors & Caregiving, Disability & Transport, Legal & Safety, Community Support. Case-insensitive, but the name must match exactly — a slug such as "financial-assistance" is rejected. Repeat to match any of several categories. For example
Financial Assistance agencystringoptional- Filter by the agency that runs the scheme. Case, punctuation and a bracketed abbreviation are ignored, so "Agency for Integrated Care" also matches "Agency for Integrated Care (AIC)". Repeat to match any of several agencies. For example
Agency for Integrated Care areastringoptional- Filter by planning area, case-insensitive. Repeat to match any of several areas. For example
BEDOK updated_sincestringoptional- ISO 8601 date or time; without an offset it is read as UTC. Returns only schemes whose updated_at is at or after it, including retired and inactive ones with their status, so an incremental sync can drop them. Combines with the other filters. For example
2026-10-01T00:00:00Z limitintegeroptional- Schemes per page. Defaults to 10, capped at 50. For example
20 cursorstringoptional- Opaque cursor from a previous response's next_cursor. Pass it back exactly as received; a modified or truncated cursor returns 400 rather than silently restarting from the first page. Omit for the first page.
Retrieve a scheme
GET/v1/schemes/{scheme_id}
Fetch one scheme by id. Retired and inactive schemes return 404; a scheme retired by merging into another carries the new id.
Path parameters
scheme_idstringrequired- The scheme's id, as returned in scheme_id by list or search. Ids are stable and the same in the development and production APIs, so store them as your key. For example
0kZ2mQ1xVbN4pR7t
Search schemes
POST/v1/schemes/search
Rank schemes against a natural-language description of someone's situation. Same retrieval the Schemes.sg assistant uses, without the conversational layer.
Body parameters
querystringrequired- Plain-language description of the situation, not keywords. Longer is better. For example
retired, living alone, struggling with medical bills limitintegeroptional- Results per page. Defaults to 20, capped at 50. For example
10 cursorstringoptional- Opaque cursor from a previous response's next_cursor. Pass it back exactly as received; a modified or truncated cursor returns 400 rather than silently restarting from the first page. Omit for the first page.
Scheme fields
Every scheme we return carries exactly these fields, whether it came from list, retrieve, or search. A field with no value is null rather than absent, so the set of keys never changes. A few fields hold either a single string or an array of strings, depending on the scheme — check the type column and handle both.
| Field | Type |
|---|---|
scheme_id | string |
scheme | string |
description | string |
summary | string |
eligibility | string |
who_is_it_for | string[] |
what_it_gives | string[] |
scheme_type | string[] |
agency | string |
link | string |
address | string | string[] |
phone | string | string[] |
email | string | string[] |
service_area | string |
planning_area | string | string[] |
image | string |
status | string |
updated_at | string | null |
We hold other fields internally, for review workflow and ranking. Those are deliberately not part of this contract, so our internal changes cannot break your integration.
Errors
Every failure returns the same envelope, so you can branch on error.code and never parse a message.
| Status | Code | What it means |
|---|---|---|
400 | invalid_request | The parameters are wrong. The message names which one. |
401 | missing_key | No X-API-Key header was sent. |
401 | invalid_key | The key is not one we issued. |
403 | revoked_key | The key was valid and has been turned off. Talk to us. |
404 | not_found | No such scheme, or it is no longer published. |
404 | scheme_retired | The scheme was retired and merged. The body carries merged_into. |
404 | unsupported_version | The version segment is missing or unknown. |
405 | method_not_allowed | Right path, wrong HTTP method. |
429 | rate_limited | Your per-minute budget is spent. Retry-After says when. |
500 | internal_error | Something broke on our side. Safe to retry. |
Rate limits
Each partner gets a per-minute budget, shared across all three operations: spending it on list also spends it for search. Every response tells you where you stand, so you never have to discover the limit by hitting it.
X-RateLimit-Limit- Your budget, in requests per minute.
X-RateLimit-Remaining- What is left in the current minute.
Retry-After- Seconds to wait. Only sent with a 429.
Keeping in sync
Scheme ids are stable and the same in the development and production APIs, so use scheme_id as your key. Every scheme carries updated_at: the latest of its last scrape, its last status change and its last bulk re-tag. Do one full sync, then ask only for what changed with updated_since. Those results also include schemes that were retired or went inactive, with their status, so you know what to drop. A bulk re-tagging run moves updated_at too, so updated_since picks up the new tags.
When a scheme goes away
Schemes close, and some are folded into a replacement. If you store scheme ids, handle this case: a retired scheme that was merged returns 404 with the id that replaced it, so you can follow the change instead of silently dropping the record.