We're here to help 🍌
Get an answer right away, or reach a human — your pick.
One endpoint, one JSON payload. Everything the v1 API accepts and returns.
Every request is a POST to https://v1.api.bidbanana.com with your key in the JSON body. There is no path to append — POST to the bare host exactly as written and you get JSON back.
curl https://v1.api.bidbanana.com -d '{
"api_key": "YOUR_API_KEY",
"operation": "search",
"keywords": "fruit",
"category": "open",
"state": "FL",
"max_results": 25
}'Your API key travels in the request body as api_key — not as a header. Create and manage keys on your API Settings page. Keys start with bbsk-, you can hold up to 10 per account, any of them authenticates a call, and deleting one revokes it.
Keep your key server-side. Because the key sits in the request body, any call made from a browser exposes it to whoever is looking. Read it from an environment variable and call the API from your backend.
| Parameter | Type | Required | Description |
|---|---|---|---|
| api_key | string | Required | One of your account's API keys, created on your API Settings page. Sent in the JSON body, not as a header. Never expose it in client-side code. |
| operation | string | Optional | The operation to perform. Defaults to "search". Send it explicitly so your client keeps working as operations are added. |
| keywords | string | Required | Search terms, separated by spaces, matched against titles, descriptions, agency names, solicitation IDs, industries and NAICS codes. Must be present, but may be an empty string: "" matches every record, so combined with state or updated_since_ms1970 it acts as a plain feed. |
| category | string | Optional | Which dataset to search: "open" (default), "closed", or "awarded". Awarded results carry extra award-specific fields. |
| state | string | Optional | Two-letter US state code to limit results geographically, e.g. "TX". Omit it, or send an empty string, to search all 50 states. |
| updated_since_ms1970 | integer | Optional | Milliseconds since the Unix epoch. Returns only records updated at their source after that moment, to whole-second precision — the field to use for a daily sync. |
| max_results | integer | Optional | How many results to return, as a JSON integer. Defaults to 10, ceiling 1,000. Larger values are clamped rather than rejected, and so is any value above what remains of your monthly allowance. There is no paging parameter — narrow the query instead. |
| only_all_keywords | boolean | Optional | Set true to require every keyword to appear. By default a result need only match some of them, ranked by frequency. |
The category parameter picks which dataset you search.
Active solicitations still accepting submissions. The default.
Past solicitations no longer open. Same fields as open bids.
Awarded contracts — every bid field plus the winning vendors, award value, and contract terms.
curl https://v1.api.bidbanana.com -d '{
"api_key": "YOUR_API_KEY",
"operation": "search",
"category": "awarded",
"keywords": "road construction",
"state": "TX",
"max_results": 50
}'| Field | Type | Description |
|---|---|---|
| response_id | string | Unique id for this response. Quote it in support requests. |
| response_time_ms1970 | number | When the response was generated, ms since epoch. |
| response_duration_ms | number | How long the request took to serve, in milliseconds. |
| api_specific_version | integer | Build number of the API that produced this response. |
| category | string | Echo of the category searched — "open", "closed", or "awarded". |
| keywords | string | Echo of the keywords you sent. |
| state | string | null | Echo of the state filter, if you sent one. |
| updated_since_ms1970 | integer | null | Echo of the updated-since filter, if you sent one, rounded down to the second. |
| max_results | integer | The result limit actually applied. Lower than what you sent if you asked for more than 1,000 or more than your remaining monthly allowance. |
| only_all_keywords | boolean | Echo of the AND/OR flag — always present, false if you didn't send it. |
| found_count | integer | How many results are in this response. Always matches results.length. |
| results | array | The matching records. See the bid object below. |
| error | string | Present only when the request failed, alongside HTTP 400. See Errors. |
Each entry in results carries these fields. Any of them can be null when that detail couldn't be discovered for a given opportunity — that's normal, so read them defensively.
An awarded search returns every field above plus the award-specific ones below — who won, for how much, and on what terms.
| Field | Type | Notes |
|---|---|---|
| vendors | array | null | The winning vendor(s). Each has name, email, phone, address — any of which may be null. |
| award_type | string | null | e.g. "Single Award", "Multiple Award". |
| award_intent_date | string | null | Notice of intent to award; may precede award_date. |
| budget_usd | integer | null | The actual awarded value. Distinct from the budget_low_usd–budget_high_usd estimate. |
| start_date | string | null | Contract performance start. |
| end_date | string | null | Contract performance end. |
| end_date_renews | string | boolean | null | Renewal terms as text, e.g. "2 optional 1-year extensions", or false if it does not renew. |
| pricing_type | string | null | e.g. "Unit Price", "Firm-Fixed-Price (FFP) / Lump Sum". |
| procurement_type | string | null | e.g. "Full and Open Competition". |
| set_aside_types | array | null | e.g. ["Small Business, Total Set-Aside"]. |
| agency_email | string | null | Agency-level contact, distinct from contact_email. |
| agency_phone | string | null | |
| agency_address | string | null |
Every failure is an HTTP 400 with an error string in the body. A bad key, a missing keywords, an unknown category or operation, a body that isn't a JSON object, and an exhausted monthly allowance all come back the same way: status 400, no results, and a plain-English error saying what to fix.
Two other statuses exist. 413 means the request body was over 64 KB, and 500 means an unexpected fault on our side; both also carry an error string. Requests to any path other than the bare host are answered with a 307 redirect, so POST to https://v1.api.bidbanana.com exactly as written.
Treat any non-200 status as a failure and read error for the reason. A successful response never carries an error field.
| Status | When | error |
|---|---|---|
| 400 | api_key missing or not a string | you must send an "api_key" string |
| 400 | api_key not recognised | unauthorized: the "api_key" you provided lacks permission |
| 400 | Subscription inactive | your subscription does not allow API results … |
| 400 | Monthly allowance used up | your "max_results_per_month" subscription limit (…) is exhausted for this month (…) |
| 400 | keywords missing or not a string | your query must supply a "keywords" string |
| 400 | Unknown category | unknown "category": "…". valid values: "open", "closed", "awarded". |
| 400 | Unknown operation | operation_not_found |
| 400 | Body is not a JSON object | invalid_json_must_be_object |
| 400 | Fault while running a search | internal error: … |
| 413 | Body larger than 64 KB | payload_too_large |
| 500 | Unexpected server fault | internal_error |
{
"response_id": "44967b18-0d34-4afc-a1af-240d3125eabc",
"response_time_ms1970": 1769164147215.7112,
"response_duration_ms": 0.42984999995678663,
"error": "unauthorized: the \"api_key\" you provided lacks permission"
}An API subscription includes 10,000 records per month. Records are counted per result returned, not per call — a request that returns 100 results uses 100 records, the same record returned twice counts twice, and open, closed and awarded searches all draw from the one allowance. The count resets on the 1st of each month (US Eastern).
As you near the end of the allowance, max_results is clamped to what remains — the echoed value in each response shows the limit that was applied. Once the allowance is used up, every search returns HTTP 400 with an error until the next month begins. Check your usage at any time on your API Settings page, and if you need more, talk to us about a higher limit or a partnership.
Each request returns at most 1,000 results and there is no paging parameter, so narrow large queries with state, category, or updated_since_ms1970. There is no per-request rate limit beyond the monthly allowance, though usage is monitored for abusive patterns.
Questions, edge cases, or a field you wish we returned? Email [email protected] — include the response_id and we can trace the exact call.