Developers

The Blue Collar Lead API.

Read and update a roofing company's contacts and jobs, send in leads for the AI to call, pull appointments, calls, job photos and reports, and get a signed message the moment something changes.

REST + JSON over HTTPS Company API keys with their own permissions Signed webhooks for six events
Request
curl https://www.bluecollarlead.com/api/v1/jobs?limit=1 \
  -H "Authorization: Bearer bcl_live_..."
Answer
{
  "data": [{
    "id": 4229,
    "job_number": 1042,
    "title": "Roof replacement",
    "contact": { "name": "Jordan Lee", "phone": "+14045550101" },
    "stage": { "key": "sold", "label": "Approved" },
    "value": 18500,
    "assigned_to": { "id": "emp-1", "name": "Marcus Bell" },
    "updated_at": "2026-10-19T18:05:00Z"
  }],
  "has_more": true,
  "next_cursor": "4229"
}

Get a key

Keys belong to one company. An owner of a Blue Collar Lead account makes them:

  1. Sign in, open Connections in the menu, and find API and webhooks.
  2. Press Make an API key, name it after the app that will use it, and tick only the permissions it needs.
  3. Copy the key. It starts with bcl_live_ and is shown once; we keep only a fingerprint of it.

Make one key per app, so you can turn one off without breaking the others. A key that is turned off stops working at once.

PermissionLets the key
contacts:readList and read contacts.
contacts:writeCreate and update contacts.
jobs:readList and read jobs, their notes, the boards and stages, and the team.
jobs:writeCreate and update jobs, move them between stages, add notes.
appointments:readList and read appointments.
calls:readList and read calls, their summaries and transcripts.
leads:writeSend in new leads. The AI follows up like it does for a website form.
files:readRead a job's photos, videos and documents, with download links.
reports:readRead photo reports and download their PDFs.
webhooks:manageSubscribe your own URLs to events.

No key can reach a company's settings, billing, logins or other keys, and no key can see another company.

Authentication

Send the key in the Authorization header on every request. Keep it on your server: a key does not belong in a web page or a mobile app.

curl https://www.bluecollarlead.com/api/v1/me \
  -H "Authorization: Bearer bcl_live_..."
{
  "company": { "id": "client-7f3a91c2", "name": "Peachtree Roofing Co.", "timezone": "America/New_York" },
  "api_key": { "name": "Office CRM sync", "prefix": "bcl_live_Xk3Q9a", "scopes": ["contacts:read", "jobs:write"] }
}

Basics

  • Base URL: https://www.bluecollarlead.com/api/v1. Always use www; the bare domain redirects.
  • Send and receive JSON. Send Content-Type: application/json with a body.
  • Times are ISO 8601 in UTC, like 2026-10-19T18:05:00Z. Phone numbers come back in E.164, like +14045550101; send them in any common format.
  • Creating a contact or a job takes an Idempotency-Key header (8 to 200 letters, digits, dots, dashes, colons or underscores). Retry with the same key and the record is created once.
  • The whole API is described in openapi.json. Import it into Postman or Insomnia to try every call.

Errors

Every error has the same shape, with a status code that matches:

{ "error": { "code": "missing_scope", "message": "This API key does not have the \"jobs:write\" permission." } }
StatusCodeWhat happened
400invalid_requestSomething in the request is wrong. The message says what.
401missing_api_key, invalid_api_keyNo key, a mistyped key, or a key that was turned off.
402subscription_inactiveThe company's subscription has ended.
403missing_scope, forbiddenThe key lacks the permission for this call.
404not_foundNo such record in this company, or no such endpoint.
409conflictFor example, the job belongs to a connected CRM and must be changed there.
422invalid_requestA required field is missing.
429rate_limitedToo many requests. Wait and retry.
500server_errorOur fault. Retry; tell us if it keeps happening.

Lists and syncing

Lists return newest first, 25 at a time (up to 100 with ?limit=). When has_more is true, ask again with ?cursor= set to the next_cursor you were given.

To keep another system in step, remember when you last synced and ask only for what changed since: ?updated_since=2026-10-19T00:00:00Z. Webhooks tell you the moment something happens; a sync on a schedule catches anything a webhook missed.

curl "https://www.bluecollarlead.com/api/v1/contacts?updated_since=2026-10-19T00:00:00Z&limit=100" \
  -H "Authorization: Bearer bcl_live_..."

Rate limits

Each key may make 120 requests a minute. Every answer carries RateLimit and RateLimit-Policy headers; past the limit you get a 429 until the window resets. Page through big lists with limit=100 rather than fetching records one at a time.

Endpoints

CallWhat it doesPermission
GET/meThe company and key in use.any
GET/contactsList contacts. Filters: q, phone, email, updated_since.contacts:read
GET/contacts/{id}One contact.contacts:read
POST/contactsCreate a contact. phone is required.contacts:write
PATCH/contacts/{id}Update name, email, address, company, notes, type or tags.contacts:write
GET/jobsList jobs. Filters: stage, status, pipeline_id, assigned_to, contact_id, job_number, q, updated_since.jobs:read
GET/jobs/{id}One job.jobs:read
POST/jobsCreate a job.jobs:write
PATCH/jobs/{id}Update a job's details (not its stage).jobs:write
POST/jobs/{id}/stageMove a job to another stage.jobs:write
GET/jobs/{id}/notesA job's notes, newest first.jobs:read
POST/jobs/{id}/notesAdd a note.jobs:write
GET/pipelinesThe boards and their stages (the stage keys to use).jobs:read
GET/teamActive team members (ids to assign jobs to).jobs:read
GET/jobs/{id}/filesPhotos, videos and documents, each with a download link that works for 15 minutes.files:read
GET/jobs/{id}/reportsA job's photo reports.reports:read
GET/reports/{id}One report.reports:read
GET/reports/{id}/pdfThe report as a PDF.reports:read
GET/reports/{id}/zipThe PDF plus every photo and video, in one ZIP.reports:read
GET/appointmentsList appointments. Filters: from, to, status, updated_since.appointments:read
GET/appointments/{id}One appointment.appointments:read
GET/callsList calls. Filters: phone, updated_since.calls:read
GET/calls/{id}One call. Add ?include=transcript for what was said.calls:read
POST/leadsSend in a new lead for the AI to follow up.leads:write
GET/webhooks/eventsThe events you can subscribe to.webhooks:manage
GET/webhooksYour subscriptions.webhooks:manage
POST/webhooksSubscribe a URL to events. Returns its signing secret once.webhooks:manage
DELETE/webhooks/{id}Remove a subscription.webhooks:manage
POST/webhooks/{id}/testSend a test event (ping).webhooks:manage

Every field of every record is in the OpenAPI spec.

Jobs, in practice

A job made through the API is made exactly like one typed in on the board: it is checked, linked to the contact with that phone number (or a new one), numbered when it passes the lead stage, and the assigned person is told.

Create a job
curl -X POST https://www.bluecollarlead.com/api/v1/jobs \
  -H "Authorization: Bearer bcl_live_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-58213" \
  -d '{
    "title": "Roof replacement",
    "contact": { "name": "Jordan Lee", "phone": "404-555-0101" },
    "address": "412 Maple Ridge Dr, Roswell, GA",
    "value": 18500,
    "assigned_to": "emp-1",
    "note": "Wants GAF Timberline, charcoal."
  }'
Move it when the contract is signed
curl -X POST https://www.bluecollarlead.com/api/v1/jobs/4229/stage \
  -H "Authorization: Bearer bcl_live_..." \
  -H "Content-Type: application/json" \
  -d '{ "stage": "sold" }'

Use GET /pipelines for the stage keys of each board, and GET /team for the ids to put in assigned_to. Custom fields go in fields, by the field's key.

Leads

POST /leads is the same door as a website form. The lead is checked for bots, spam and the do-not-call list, a job is made on the board, and the AI calls or texts within the company's calling hours and daily limit. The same number twice within 10 minutes counts once.

curl -X POST https://www.bluecollarlead.com/api/v1/leads \
  -H "Authorization: Bearer bcl_live_..." \
  -H "Content-Type: application/json" \
  -d '{ "name": "Casey Morgan", "phone": "678-555-0144", "address": "9 Elm Rd, Marietta, GA",
        "service": "Roof repair", "message": "Leak over the kitchen after the storm." }'

Only send people who asked to be contacted. For a customer you already work with, use POST /contacts or POST /jobs, which never call anyone.

Webhooks

Subscribe an https URL on your system to the events you care about. We POST JSON to it within seconds of the event.

EventWhen
job.createdA job was created: in the app, by the AI from a call or text, from a lead, or through the API.
job.stage_changedA job moved to another stage. Carries from_stage and to_stage.
contact.createdA contact was created in the app or through the API.
lead.receivedA lead came in from a website form or the API.
appointment.bookedThe AI booked an appointment.
optout.requestedSomeone asked not to be contacted again, on a call or by texting STOP. data.channel says which. Stop calling and texting them.
curl -X POST https://www.bluecollarlead.com/api/v1/webhooks \
  -H "Authorization: Bearer bcl_live_..." \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://example.com/hooks/bcl", "events": ["job.created", "job.stage_changed"] }'

The answer includes a secret (whsec_...) shown only then. Owners can also add webhooks from the app.

What arrives

POST /hooks/bcl
X-BCL-Event: job.stage_changed
X-BCL-Delivery: dlv_Q2x7mK0aP1sT
X-BCL-Signature: sha256=5f0c...

{
  "id": "dlv_Q2x7mK0aP1sT",
  "event": "job.stage_changed",
  "created_at": "2026-10-19T18:05:02.114Z",
  "data": { "job": { "id": 4229, "stage": { "key": "sold", "label": "Approved" } }, "from_stage": "quoted", "to_stage": "sold" }
}
  • Answer with any 2xx within 8 seconds. Do the slow work after you answer.
  • We retry up to 4 times over about 30 seconds after a timeout, a 408, a 429 or a 5xx. Other answers are not retried.
  • X-BCL-Delivery is the same on every retry, so you can ignore a repeat.
  • After 100 failed deliveries in a row we turn the subscription off. Add it again once your end is fixed.
  • Addresses that point at private networks are refused.

Verifying a webhook

Compute an HMAC-SHA256 of the raw request body with your secret, and compare it with X-BCL-Signature in constant time. Use the raw bytes, before any JSON parsing.

Node.js
const crypto = require('crypto');

function verified(rawBody, header, secret) {
  const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
  const a = Buffer.from(expected), b = Buffer.from(String(header || ''));
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Python
import hmac, hashlib

def verified(raw_body: bytes, header: str, secret: str) -> bool:
    expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, header or "")

Rules worth knowing

  • Stages move with their side effects. Moving a job to the company's finished stage can queue the customer's review request and push the job to their connected CRM, exactly like dragging the card. That is why the stage changes only through POST /jobs/{id}/stage, never PATCH.
  • Jobs from a connected CRM (GoHighLevel, JobTread) belong to that CRM. Moving them answers 409; change them there and they sync back.
  • Phone numbers tie the system together. A contact's phone cannot be changed through the API, because calls, texts and jobs find a person by it.
  • Appointments are read only in this version. The AI books them against the company's calendars and rules; send a lead or create a job and the company takes it from there.
  • Download links expire. File links work for 15 minutes. Ask for the file list again to get fresh ones.

Versions and help

This is version 1. Within it we only add things: new fields, new endpoints, new events. We never rename or remove a field, so ignore fields you do not know. Anything that would break an integration gets a new version, and the old one keeps working while you move.

Questions, or something you need the API to do: [email protected].

Building for a roofing company?

Ask the owner for a key from Connections, API and webhooks, and start with GET /me.