The Bid Developer API (1.0.0)

Download OpenAPI specification:

REST API for estate agent partners to submit referrals, track property progress, access documents, and receive real-time webhook notifications.

Getting Started

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.

Quickstart

1. Get Your Credentials

Your Bid account manager will provide:

  • client_id — UUID identifying your integration
  • client_secret — starts with bid_cs_ (store securely, never expose in client-side code)

2. Exchange for an Access Token

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"]
  }
}

3. Verify Your Connection

curl https://api.thebid.uk/v1/health

Returns {"status":"ok","version":"1","timestamp":"..."} — no auth needed.

4. List Your Branches

curl https://api.thebid.uk/v1/branches \
  -H "Authorization: Bearer bid_at_..."

Cache the branch_id values — you'll need one to submit referrals.

5. Submit a Referral

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"
  }
}

Authentication

The Bid API uses OAuth 2.0 Client Credentials for machine-to-machine authentication.

How It Works

  1. You receive a client_id and client_secret from Bid
  2. Exchange them for a short-lived access token (1 hour)
  3. Use the access token as a Bearer token on all API requests
  4. When it expires, exchange again — no refresh tokens needed

Token Exchange

cURL:

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>();

Handling Token Expiry

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;
}

Storing Credentials

  • Never commit client_secret to source control
  • Use environment variables or a secrets manager
  • The client_secret starts with bid_cs_ — if you see this in logs, rotate immediately

Rate Limits

  • 60 requests per minute per client (configurable by Bid)
  • Token exchange: 10 requests per minute
  • On 429 Too Many Requests: back off and retry after the rate window resets (1 minute)

Permissions

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

Webhooks

Receive real-time notifications when referral status changes, properties reach milestones, or new buyers are registered.

Registering a Subscription

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:

  • URL must use https://
  • No localhost, private IPs (10.*, 172.16-31.*, 192.168.*), or .local domains
  • The secret is returned once on creation — store it immediately

Available Events

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

Payload Envelope

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.

Verifying Signatures

Every delivery includes three headers:

  • X-Bid-Event — the event type
  • X-Bid-Timestamp — Unix epoch seconds at delivery time
  • X-Bid-Signaturesha256=<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));
}

Retry Behaviour

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.

Documents

Access property documents (legal packs, AML reports, etc.) via short-lived signed URLs.

Listing Documents

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 format
  • file_size and mime_type are intentionally not included (not stored on the document record)
  • Requires both properties:read and documents:read permissions

Getting a Signed URL

curl -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
  }
}

Download Pattern

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));

Security Considerations

  • Do not cache signed URLs — they expire in 120 seconds
  • Do not log signed URLs — they grant temporary access to the file
  • Each access is audit-logged (agent, client, document, IP, timestamp)
  • Cross-agent access returns 404 (not 403) to prevent information leakage

Errors

All errors return a JSON envelope with an error object containing code, message, and optionally details.

Error Codes

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 Response Shape

{
  "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
  • 5xx errors never expose internal messages — always "Internal server error"

CONFLICT (409) — Duplicate Referrals

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"
    }
  }
}

Idempotency

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" \
  ...
  • Same key + same client = replayed original response (no duplicate created)
  • Keys expire after 24 hours
  • Missing key on POST = 400 VALIDATION_ERROR

Handling Errors Programmatically

const 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)
}

Health

Health check endpoint

Health check

Unauthenticated endpoint to verify the API is reachable.

Responses

Response samples

Content type
application/json
{
  • "status": "ok",
  • "version": "1",
  • "timestamp": "2019-08-24T14:15:22Z"
}

OAuth

Token exchange endpoints

Exchange credentials for access token

Request Body schema: application/json
required
client_id
required
string <uuid>
client_secret
required
string
grant_type
required
string
Value: "client_credentials"

Responses

Request samples

Content type
application/json
{
  • "client_id": "5b3fa7ba-57d3-4017-a65b-d57dcd2db643",
  • "client_secret": "bid_cs_...",
  • "grant_type": "client_credentials"
}

Response samples

Content type
application/json
{
  • "data": {
    },
  • "request_id": "266ea41d-adf5-480b-af50-15b940c2b846"
}

Referrals

Referral management endpoints

List referrals

Returns referrals for the authenticated agent with pagination.

Authorizations:
BearerAuth
query Parameters
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>

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "pagination": {
    },
  • "request_id": "string"
}

Create a referral

Submit a new property referral. Requires the Idempotency-Key header. Returns 409 CONFLICT if a duplicate address is found.

Authorizations:
BearerAuth
header Parameters
Idempotency-Key
required
string <= 255 characters

Unique key to prevent duplicate submissions (UUID recommended)

Request Body schema: application/json
required
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>

Responses

Request samples

Content type
application/json
{
  • "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"
}

Response samples

Content type
application/json
{
  • "data": {
    },
  • "request_id": "string"
}

Properties

Property data endpoints

List or get properties

Authorizations:
BearerAuth
query Parameters
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).

Responses

Response samples

Content type
application/json
{
  • "data": {
    },
  • "pagination": {
    },
  • "request_id": "string"
}

Document Access

Document signed URL endpoints

Generate a signed document URL

Returns a short-lived signed URL (120 seconds) for downloading the document. Each access is audit-logged.

Authorizations:
BearerAuth
path Parameters
document_id
required
string <uuid>

Responses

Response samples

Content type
application/json
{}

Webhook Subscriptions

Webhook subscription management endpoints

List webhook subscriptions

Authorizations:
BearerAuth

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "request_id": "string"
}

Create a webhook subscription

Register a URL to receive event notifications. The secret is returned once on creation — store it securely for signature verification.

Authorizations:
BearerAuth
Request Body schema: application/json
required
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"

Responses

Request samples

Content type
application/json
{}

Response samples

Content type
application/json
{
  • "data": {
    },
  • "request_id": "string"
}

Delete a webhook subscription

Authorizations:
BearerAuth
query Parameters
subscription_id
required
string <uuid>

Responses

Response samples

Content type
application/json
{
  • "data": {
    },
  • "request_id": "string"
}