The GovBidReadyNow API gives your systems server-to-server access to your organization’s workspace: search opportunities with the same criteria as the app, keep pursuits in sync with your CRM, and read proposals and contracts. Keys are created by your owners and administrators, scoped to the access you choose, and limited to their creator’s current role.
Authentication
Base URL: https://app.govbidreadynow.com/api/v1/gov/public/v1. Version 1.0.0; breaking changes will ship under a new /v2 path.
Owners and administrators create keys in Settings → API keys. Send the key as Authorization: Bearer fbk_… (or X-API-Key: fbk_…). The full key is shown once; GovBidReadyNow stores only a keyed hash. Every key has an expiry of at most one year and can be rotated or revoked at any time.
A key acts for the person who created it. It never has more access than that person’s current role, and it stops working if they leave the organization or their account or the organization is suspended. Each request needs both the operation’s scope and a role that permits the action.
Browsers are not supported. The API sends no CORS headers; call it from your server, scripts or terminal and keep keys out of web pages, mobile apps and source control.
Scopes
Scope
Allows
opportunities:read
Search opportunities and read opportunity details.
pursuits:read
List pursuits and their stages.
pursuits:write
Save, unsave, add public opportunities, start responses, change capture stages and record submissions or outcomes.
proposals:read
Read proposal metadata and section text. Pricing is never included.
contracts:read
Read awarded contracts.
comments:read
Read opportunity comments.
comments:write
Add opportunity comments.
The API does not provide AI generation, agents, billing, member management or document uploads.
Rate limits
60 requests per minute per key and 600 per organization, measured over a sliding minute. Every response includes X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (seconds until the window resets). A 429 response includes Retry-After; back off and retry after that many seconds. Repeated failed authentication from one address is also limited.
Pagination
List and search operations take offset (≤ 10,000) and limit (1–100, default 25) and return total and nextOffset, which is null on the last page.
Errors
Errors share one shape: {"error": {"code": "not_found", "message": "Record not found in this workspace."}}. Branch on code; messages are for people.
Status
Code
Meaning
400
bad_request
Malformed header such as an invalid Idempotency-Key.
401
invalid_api_key
Missing, malformed, expired or revoked key, or its creator no longer has access. The response never says which.
402
payment_required
The organization is read-only: reads and search still work, writes do not.
The key lacks the scope, the role does not permit the action, or the organization is suspended.
404
not_found
Not in this organization. Records in other organizations are always 404.
409
conflict · version_conflict
The record changed, a recorded stage needs its dedicated action, or an idempotent request is still running.
412
precondition_failed
If-Match did not match the current ETag.
413
payload_too_large
Request bodies are limited to 256 KB.
422
validation_error
The body or query is invalid; details list the failing fields (values are never echoed).
429
rate_limited
Too many requests. Wait for Retry-After seconds.
Idempotency and versions
Send an Idempotency-Key header (1–200 letters, digits, - _ . :) with any POST or PUT. Retrying with the same key and body within 24 hours returns the stored result with Idempotent-Replayed: true instead of acting twice; reusing a key for a different body returns 422.
GET /opportunities/{id} returns an ETag. Send it back as If-Match (or send version in the body) so a write fails with 412/409 instead of overwriting someone else’s change. Submitted, Lost and Cancelled are recorded only with /submission and /outcome, which keep their dates, receipts and reasons; awards are recorded in the app.
import json, os, urllib.request
BASE = "https://app.govbidreadynow.com/api/v1/gov/public/v1"
KEY = os.environ["FED_BIDDER_API_KEY"]
def call(method, path, body=None, idempotency_key=None):
headers = {"Authorization": f"Bearer {KEY}", "Content-Type": "application/json"}
if idempotency_key:
headers["Idempotency-Key"] = idempotency_key
data = json.dumps(body).encode() if body is not None else None
request = urllib.request.Request(BASE + path, data=data, method=method, headers=headers)
with urllib.request.urlopen(request, timeout=30) as response:
return json.load(response)
# Page through every open opportunity that matches your criteria.
offset = 0
while offset is not None:
page = call("POST", "/opportunities/search", {"criteria": {"q": "zero trust", "response": "open"}, "offset": offset, "limit": 100})
for item in page["items"]:
print(item["id"], item["deadline"], item["title"])
offset = page["nextOffset"]
# Save an opportunity exactly once, even if this script is retried.
call("PUT", "/opportunities/OPPORTUNITY_ID/saved", {"saved": True}, idempotency_key="crm-sync-OPPORTUNITY_ID")
Terminal with jq
# Who am I? Shows the organization, key scopes and expiry.
curl -sS https://app.govbidreadynow.com/api/v1/gov/public/v1/me -H "Authorization: Bearer $FED_BIDDER_API_KEY" | jq
# Pursuits due soonest, one per line.
curl -sS "https://app.govbidreadynow.com/api/v1/gov/public/v1/pursuits?sort=deadline&limit=20" \
-H "Authorization: Bearer $FED_BIDDER_API_KEY" \
| jq -r '.items[] | [.deadline, .stage, .title] | @tsv'
# Move a pursuit to Go decision only if nobody changed it since you read it.
ETAG=$(curl -sS -D - -o /dev/null https://app.govbidreadynow.com/api/v1/gov/public/v1/opportunities/OPPORTUNITY_ID \
-H "Authorization: Bearer $FED_BIDDER_API_KEY" | awk -F': ' 'tolower($1)=="etag"{print $2}' | tr -d '\r')
curl -sS https://app.govbidreadynow.com/api/v1/gov/public/v1/opportunities/OPPORTUNITY_ID/stage \
-H "Authorization: Bearer $FED_BIDDER_API_KEY" -H "If-Match: $ETAG" \
-H 'Content-Type: application/json' -d '{"stage": "Go decision"}'