Download OpenAPI specification:
REST API for estate agent partners to submit referrals, track property progress, access documents, and receive real-time webhook notifications.
Your Bid account manager will provide a client_id and client_secret. See the
Quickstart guide to make your first API call in 5 minutes.
Your Bid account manager will provide:
client_id — UUID identifying your integrationclient_secret — starts with bid_cs_ (store securely, never expose in client-side code)curl -X POST https://api.thebid.uk/v1/oauth/token \
-H "Content-Type: application/json" \
-d '{
"client_id": "YOUR_CLIENT_ID",
"client_secret": "YOUR_CLIENT_SECRET",
"grant_type": "client_credentials"
}'
Response:
{
"data": {
"access_token": "bid_at_...",
"token_type": "Bearer",
"expires_in": 3600,
"permissions": ["referrals:read", "referrals:write", "properties:read"]
}
}
curl https://api.thebid.uk/v1/health
Returns {"status":"ok","version":"1","timestamp":"..."} — no auth needed.
curl https://api.thebid.uk/v1/branches \
-H "Authorization: Bearer bid_at_..."
Cache the branch_id values — you'll need one to submit referrals.
curl -X POST https://api.thebid.uk/v1/referrals \
-H "Authorization: Bearer bid_at_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"branch_id": "YOUR_BRANCH_ID",
"house_name_or_number": "42",
"street_name": "High Street",
"town_or_city": "London",
"post_code": "SW1A 1AA",
"customer_name": "Jane Smith",
"customer_email": "jane@example.com"
}'
Response (201):
{
"data": {
"referral_id": "uuid-here",
"status": "new",
"created_at": "2026-05-23T14:00:00Z"
}
}
The Bid API uses OAuth 2.0 Client Credentials for machine-to-machine authentication.
client_id and client_secret from BidcURL:
curl -X POST https://api.thebid.uk/v1/oauth/token \
-H "Content-Type: application/json" \
-d '{
"client_id": "your-uuid",
"client_secret": "bid_cs_your_secret",
"grant_type": "client_credentials"
}'
Node/TypeScript:
const res = await fetch("https://api.thebid.uk/v1/oauth/token", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
client_id: process.env.BID_CLIENT_ID,
client_secret: process.env.BID_CLIENT_SECRET,
grant_type: "client_credentials",
}),
});
const { data } = await res.json();
// data.access_token, data.expires_in
C#:
var client = new HttpClient();
var response = await client.PostAsync(
"https://api.thebid.uk/v1/oauth/token",
new StringContent(JsonSerializer.Serialize(new {
client_id = Environment.GetEnvironmentVariable("BID_CLIENT_ID"),
client_secret = Environment.GetEnvironmentVariable("BID_CLIENT_SECRET"),
grant_type = "client_credentials"
}), Encoding.UTF8, "application/json"));
var result = await response.Content.ReadFromJsonAsync<TokenResponse>();
Tokens expire after 3600 seconds (1 hour). On receiving a 401 response, re-authenticate:
async function apiCall(url: string, options: RequestInit = {}) {
let token = await getToken(); // cached token
let res = await fetch(url, {
...options,
headers: { ...options.headers, Authorization: `Bearer ${token}` },
});
if (res.status === 401) {
token = await refreshToken(); // exchange credentials again
res = await fetch(url, {
...options,
headers: { ...options.headers, Authorization: `Bearer ${token}` },
});
}
return res;
}
client_secret to source controlclient_secret starts with bid_cs_ — if you see this in logs, rotate immediately429 Too Many Requests: back off and retry after the rate window resets (1 minute)Your client is issued with specific permissions. Calling an endpoint you don't have permission for returns 403 FORBIDDEN:
| Permission | Endpoints |
|---|---|
referrals:read |
GET /v1/referrals |
referrals:write |
POST /v1/referrals |
properties:read |
GET /v1/properties |
buyers:read |
GET /v1/buyers |
documents:read |
POST /v1/documents/{id}/access-url |
webhooks:manage |
GET/POST/DELETE /v1/webhooks |
Receive real-time notifications when referral status changes, properties reach milestones, or new buyers are registered.
curl -X POST https://api.thebid.uk/v1/webhooks \
-H "Authorization: Bearer bid_at_..." \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-server.com/webhooks/bid",
"events": ["referral.created", "referral.status_changed", "property.status_changed"]
}'
Requirements:
https://localhost, private IPs (10.*, 172.16-31.*, 192.168.*), or .local domainssecret is returned once on creation — store it immediately| Event | Trigger |
|---|---|
referral.created |
New referral submitted |
referral.status_changed |
Referral status updated |
property.status_changed |
Property status category changes |
property.milestone_completed |
A milestone is marked complete |
buyer.created |
New buyer registered on a property |
buyer.offer_updated |
Buyer's offer status changes |
Every delivery uses this structure:
{
"version": "1",
"event_id": "uuid",
"event_type": "referral.status_changed",
"created_at": "2026-05-23T14:30:00Z",
"data": {
"referral_id": "uuid",
"old_status": "new",
"new_status": "contacted"
}
}
Branch on version for forward compatibility. Treat missing version as "1".
No PII in payloads — only IDs and status values. Fetch full details via the GET endpoints.
Every delivery includes three headers:
X-Bid-Event — the event typeX-Bid-Timestamp — Unix epoch seconds at delivery timeX-Bid-Signature — sha256=<hex HMAC>The signed string is: timestamp + "." + raw_body
Node/TypeScript:
import crypto from "crypto";
function verifyWebhook(body: string, headers: Headers, secret: string): boolean {
const timestamp = headers.get("x-bid-timestamp")!;
const signature = headers.get("x-bid-signature")!;
// Reject stale deliveries (replay protection)
if (Math.abs(Date.now() / 1000 - parseInt(timestamp)) > 300) {
return false;
}
const expected = "sha256=" + crypto
.createHmac("sha256", secret)
.update(`${timestamp}.${body}`)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected),
);
}
C#:
bool VerifyWebhook(string body, string timestamp, string signature, string secret) {
if (Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - long.Parse(timestamp)) > 300)
return false;
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes($"{timestamp}.{body}"));
var expected = "sha256=" + BitConverter.ToString(hash).Replace("-", "").ToLower();
return CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(signature),
Encoding.UTF8.GetBytes(expected));
}
Failed deliveries (non-2xx or timeout) are retried with exponential backoff:
| Attempt | Retry after |
|---|---|
| 1 | 30 seconds |
| 2 | 2 minutes |
| 3 | 15 minutes |
| 4 | 1 hour |
| 5 | 4 hours |
After 5 failed attempts, the event is marked as dead-letter (no further retries).
[Bid Webhook] → [Your HTTPS endpoint] → [Message Queue] → [Worker]
Accept the delivery immediately (return 200), queue it for processing. This avoids timeouts if your processing is slow.
Access property documents (legal packs, AML reports, etc.) via short-lived signed URLs.
Documents are included in property responses when you pass ?include=documents:
curl "https://api.thebid.uk/v1/properties?property_id=UUID&include=documents" \
-H "Authorization: Bearer bid_at_..."
Response includes:
{
"data": {
"property_id": "...",
"documents": [
{
"id": "document-uuid",
"name": "1716480000-abc123-legal-pack.pdf",
"type": "Legal Pack",
"uploaded_at": "2026-05-23T10:00:00Z",
"modified_at": "2026-05-23T10:00:00Z"
}
]
}
}
Notes:
name is the verbatim last path segment from storage — do not assume a stable formatfile_size and mime_type are intentionally not included (not stored on the document record)properties:read and documents:read permissionscurl -X POST "https://api.thebid.uk/v1/documents/DOCUMENT_ID/access-url" \
-H "Authorization: Bearer bid_at_..."
Response:
{
"data": {
"url": "https://storage.example.com/sign/documents/...",
"expires_in": 120
}
}
The signed URL expires after 120 seconds. Download immediately:
const { data } = await bidApi.post(`/v1/documents/${docId}/access-url`);
const fileResponse = await fetch(data.url);
const buffer = await fileResponse.arrayBuffer();
fs.writeFileSync(`downloads/${filename}`, Buffer.from(buffer));
All errors return a JSON envelope with an error object containing code, message, and optionally details.
| HTTP | Code | Description |
|---|---|---|
| 400 | VALIDATION_ERROR |
Request body failed validation or missing required header |
| 401 | UNAUTHORIZED |
Missing, invalid, or expired token |
| 403 | FORBIDDEN |
Token lacks required permission |
| 404 | NOT_FOUND |
Resource not found (or cross-agent access) |
| 405 | METHOD_NOT_ALLOWED |
HTTP method not supported on this endpoint |
| 409 | CONFLICT |
Duplicate referral detected |
| 429 | RATE_LIMITED |
Too many requests — wait and retry |
| 500 | INTERNAL_ERROR |
Server error (message is always "Internal server error") |
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Missing required field: branch_id",
"details": ["Missing required field: branch_id", "Missing required field: post_code"],
"request_id": "uuid"
}
}
details is only present on 4xx errors"Internal server error"When submitting a referral that matches an existing address:
Open referral exists:
{
"error": {
"code": "CONFLICT",
"message": "An open referral already exists for this address",
"details": {
"reason": "open_referral",
"referral_id": "existing-uuid",
"status": "contacted"
}
}
}
Active property at address:
{
"error": {
"code": "CONFLICT",
"message": "An active property exists for this address",
"details": {
"reason": "active_property",
"property_id": "property-uuid",
"referral_id": "referral-uuid",
"status_category": "instructed"
}
}
}
All POST endpoints require an Idempotency-Key header (max 255 characters, UUID recommended):
curl -X POST .../v1/referrals \
-H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
...
400 VALIDATION_ERRORconst res = await fetch(url, options);
if (res.status === 401) {
// Re-authenticate and retry
token = await getNewToken();
return retry(url, options, token);
}
if (res.status === 429) {
// Back off 60 seconds (rate window)
await sleep(60_000);
return retry(url, options, token);
}
if (res.status === 409) {
const { error } = await res.json();
// Use error.details.reason to decide: link to existing or alert user
}
if (res.status >= 500) {
// Retry with exponential backoff (Bid is having issues)
}
| client_id required | string <uuid> |
| client_secret required | string |
| grant_type required | string Value: "client_credentials" |
{- "client_id": "5b3fa7ba-57d3-4017-a65b-d57dcd2db643",
- "client_secret": "bid_cs_...",
- "grant_type": "client_credentials"
}{- "data": {
- "access_token": "bid_at_a1b2c3d4e5f6...",
- "token_type": "Bearer",
- "expires_in": 3600,
- "permissions": [
- "referrals:read",
- "referrals:write",
- "properties:read"
]
}, - "request_id": "266ea41d-adf5-480b-af50-15b940c2b846"
}Returns referrals for the authenticated agent with pagination.
| page | integer Default: 1 |
| per_page | integer <= 200 Default: 50 |
| referral_id | string <uuid> Return a single referral by ID |
| status | string |
| branch_id | string <uuid> |
| created_after | string <date-time> |
{- "data": [
- {
- "referral_id": "2d946960-d63d-4137-b67f-4dc8d4a3067c",
- "created_at": "2019-08-24T14:15:22Z",
- "status": "new",
- "branch_id": "7a4e8e99-89f2-4a0f-b66c-fc595dda2dbc",
- "post_code": "SW1A 1AA",
- "house_name_or_number": "10",
- "flat_number": "string",
- "street_name": "string",
- "locality": "string",
- "town_or_city": "London",
- "property_type": "string",
- "number_of_bedrooms": "string",
- "tenure": "string",
- "customer_name": "string",
- "customer_phone": "string",
- "customer_email": "string"
}
], - "pagination": {
- "page": 0,
- "per_page": 0,
- "total": 0
}, - "request_id": "string"
}Submit a new property referral. Requires the Idempotency-Key header.
Returns 409 CONFLICT if a duplicate address is found.
| Idempotency-Key required | string <= 255 characters Unique key to prevent duplicate submissions (UUID recommended) |
| branch_id required | string <uuid> |
| house_name_or_number required | string |
| street_name | string |
| locality | string |
| town_or_city required | string |
| post_code required | string |
| flat_number | string |
| property_type | string |
| number_of_bedrooms | string |
| tenure | string |
| customer_name required | string |
| customer_phone | string |
| customer_email | string <email> |
{- "branch_id": "7a4e8e99-89f2-4a0f-b66c-fc595dda2dbc",
- "house_name_or_number": "string",
- "street_name": "string",
- "locality": "string",
- "town_or_city": "string",
- "post_code": "string",
- "flat_number": "string",
- "property_type": "string",
- "number_of_bedrooms": "string",
- "tenure": "string",
- "customer_name": "string",
- "customer_phone": "string",
- "customer_email": "user@example.com"
}{- "data": {
- "referral_id": "2d946960-d63d-4137-b67f-4dc8d4a3067c",
- "status": "new",
- "created_at": "2019-08-24T14:15:22Z"
}, - "request_id": "string"
}| page | integer Default: 1 |
| per_page | integer <= 200 Default: 50 |
| property_id | string <uuid> |
| status_category | string |
| branch_id | string <uuid> |
| include | string Example: include=documents Comma-separated includes. Supports "documents" (requires documents:read permission). |
{- "data": {
- "property_id": "05003a8a-8f3c-454b-8884-a906ec46f5f5",
- "referral_id": "2d946960-d63d-4137-b67f-4dc8d4a3067c",
- "branch_id": "7a4e8e99-89f2-4a0f-b66c-fc595dda2dbc",
- "status_category": "instructed",
- "house_name_or_number": "string",
- "street_name": "string",
- "town_or_city": "string",
- "post_code": "string",
- "property_type": "string",
- "sale_type": "string",
- "milestones": [
- {
- "milestone": "string",
- "completed_at": "2019-08-24T14:15:22Z",
- "is_skipped": true
}
], - "documents": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "type": "string",
- "uploaded_at": "2019-08-24T14:15:22Z",
- "modified_at": "2019-08-24T14:15:22Z"
}
]
}, - "pagination": {
- "page": 0,
- "per_page": 0,
- "total": 0
}, - "request_id": "string"
}{- "data": [
- {
- "subscription_id": "aa11a4c2-a467-43db-b413-c4ab0f5cf627",
- "events": [
- "string"
], - "secret": "string",
- "is_active": true,
- "created_at": "2019-08-24T14:15:22Z"
}
], - "request_id": "string"
}Register a URL to receive event notifications. The secret is returned once on creation — store it securely for signature verification.
| url required | string <uri> Must be a public HTTPS URL (no localhost, private IPs) |
| events required | Array of strings Items Enum: "referral.created" "referral.status_changed" "property.status_changed" "property.milestone_completed" "buyer.created" "buyer.offer_updated" |
{- "events": [
- "referral.created"
]
}{- "data": {
- "subscription_id": "aa11a4c2-a467-43db-b413-c4ab0f5cf627",
- "events": [
- "string"
], - "secret": "string",
- "is_active": true,
- "created_at": "2019-08-24T14:15:22Z"
}, - "request_id": "string"
}