Getting Started
Read Bing’s results for a query as JSON, check where your site ranks for the queries you care about, then widen the research with suggestions, news and videos.
1. Get your API key
Create an account and generate a FetchLayer API key from your dashboard. There is no Microsoft account, Azure subscription or Bing API key involved.
Create free account →2. Search
Every organic result comes back with its position, title, URL, site name, snippet, date and sitelinks, with what Bing showed alongside: the answer box or AI overview with its sources, the knowledge panel, People also ask questions, related searches, top stories and videos. Ads come back separately and never take a position.
curl -X POST https://api.fetchlayer.dev/bing/search \
-H "Authorization: Bearer ss-your-key" \
-H "Content-Type: application/json" \
-d '{"query":"open source crm","limit":10}'Going deeper
limit above 10 reads more result pages — up to 200 results — and each page returned counts as one request (pagesScraped). To continue later, send nextCursor back as cursor with the same query and filters. 3. Check where your site ranks
For SEO rank tracking, don’t page through search yourself. /rank-check scans up to 200 positions for a domain or a URL prefix and returns whether it was found, its best position, and every match with the page it was on.
curl -X POST https://api.fetchlayer.dev/bing/rank-check \
-H "Authorization: Bearer ss-your-key" \
-H "Content-Type: application/json" \
-d '{"query":"reddit api","target":"fetchlayer.dev","depth":200}'One credit at any depth
Rankings depend on the searcher
market: "en-US") on desktop unless you set a market or device, and the same query ranks differently in en-GB or de-DE. Bing’s positions also move noticeably between regions and even between two checks minutes apart, so track a query over time rather than reading one check as fixed. By default the scan stops after the first result page that contains a match. Set allMatches to scan the full depth and see every page of yours that ranks. found: false always means the whole depth was examined.
4. Expand the research
/suggestions returns what Bing’s search box offers for a partial query — the queries worth tracking next. /news searches Bing News with a time range down to the past hour, and /videos searches Bing Videos (without a time filter).
curl -X POST https://api.fetchlayer.dev/bing/news \
-H "Authorization: Bearer ss-your-key" \
-H "Content-Type: application/json" \
-d '{"query":"openai","limit":10,"timeRange":"day"}'Tip
/resolve-url — it returns the query, market, safe search, time range and offset the URL carries, or where a result link leads, without contacting Bing, and it does not consume a credit. Errors and retries
A 400 names the field to fix. Unrecognised fields are rejected rather than ignored — and /suggestions takes a market but nothing else, while /videos takes no time range. A 503 means Bing could not be read within the time allowed; it is always worth retrying, and a rank check never reports “not found” in its place. A search that matches nothing is a 200 with an empty list. Read notes on every successful response too.
Error responses
Every error, on every route, is JSON in one shape: error (what went wrong, in words), status, code, retryable and requestId, which also comes as the X-Request-Id header. Branch on code and retryable rather than on the text; on a 5xx the text is generic. Quote requestId when reporting a problem.
{
"error": "Missing required parameter: url",
"status": 400,
"missing": ["url"],
"code": "invalid_input",
"retryable": false,
"requestId": "922874e3-c0d0-40d8-8866-d198af132420"
}| code | Status | Retryable | Meaning |
|---|---|---|---|
| invalid_input | 400 | no | The request needs fixing; error names the field. A missing required parameter also lists it in missing. |
| unauthorized | 401 | no | The API key is missing or not valid. |
| insufficient_credits | 402 | no | Nothing left to spend: no credits, a used-up free trial, or a monthly allowance and credit balance both spent. error says which. Waiting does not help; topping up does. |
| forbidden | 403 | no | This key or account is not allowed this request. |
| not_found | 404 | no | What the request names does not exist or is not public. |
| expired | 410 | no | What the request refers to is no longer available; ask for it afresh. |
| quota_exceeded | 429 | no | A request-rate limit was reached; error says which. Slow down before sending more. |
| cancelled | 499 | no | The caller closed or cancelled the request before it finished. The work was stopped and nothing was charged. |
| time_limit | 503 | yes | Your timeoutMs ran out before there was anything to return; ask again, or allow more time. |
| temporarily_unavailable | 503 | yes | Not available right now; ask again shortly. |
| posts_unavailable | 403 | no | TikTok user videos: TikTok is not showing this account’s posts publicly right now. Asking again at once returns the same answer; nothing is charged. |
| store_unreachable | 503 | yes | Store routes (Shopify, WooCommerce): the store itself can’t be reached right now — it may be offline. Ask again later. Where a store route doesn’t say so yet, an offline store is temporarily_unavailable. |
| upstream_error | 502 | yes | The source answered in a way that could not be used; ask again later. |
| internal_error | 500 | yes | Something failed on our side; ask again later, and quote requestId if it persists. |
| tool_error | — | no | MCP only: a tool call failed in a way no other code describes; error says what. |
More codes may be added; treat one you do not recognise by its retryable flag. Over MCP, a tool error carries the same fields in structuredContent, and as a JSON text block beside the message.