The BlueClerk Public API lets partners read and write contractor data — properties, jobs, tickets, estimates, invoices, and contacts — using OAuth 2.0.
This API powers integrations with Zapier, Make.com, n8n, custom internal tools, and partner apps like leads2jobs.
Status: v1 — generally available. Self-serve registration at app.blueclerk.com/developers.
- Register as a partner. Sign up at app.blueclerk.com/developers, create an OAuth app with your name, redirect URI, and scopes. You'll get a
client_idandclient_secretinstantly — no waiting. - Send your user through the OAuth consent flow. They authorize your app on their BlueClerk account.
- Exchange the auth code for an access token.
- Call the API with
Authorization: Bearer <token>.
A full curl walkthrough is in examples/curl-walkthrough.sh.
https://app.blueclerk.com/api/v1
(Future: https://api.blueclerk.com/v1 once the subdomain rewrite is live.)
BlueClerk uses the Authorization Code flow with PKCE.
https://app.blueclerk.com/oauth/authorize
?response_type=code
&client_id=YOUR_CLIENT_ID
&redirect_uri=YOUR_REDIRECT_URI
&scope=leads:write+jobs:read
&state=RANDOM_STRING
&code_challenge=PKCE_CHALLENGE
&code_challenge_method=S256
The user logs in, sees what permissions you're requesting, and clicks Allow. We redirect to your redirect_uri with ?code=AUTH_CODE&state=RANDOM_STRING.
curl -X POST https://app.blueclerk.com/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code" \
-d "code=AUTH_CODE" \
-d "redirect_uri=YOUR_REDIRECT_URI" \
-d "client_id=YOUR_CLIENT_ID" \
-d "client_secret=YOUR_CLIENT_SECRET" \
-d "code_verifier=PKCE_VERIFIER"Response:
{
"access_token": "bc_at_...",
"refresh_token": "bc_rt_...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "leads:write jobs:read"
}curl https://app.blueclerk.com/api/v1/me \
-H "Authorization: Bearer bc_at_..."Access tokens expire after 1 hour. Refresh tokens last 90 days and rotate on every use.
curl -X POST https://app.blueclerk.com/oauth/token \
-d "grant_type=refresh_token" \
-d "refresh_token=bc_rt_..." \
-d "client_id=YOUR_CLIENT_ID" \
-d "client_secret=YOUR_CLIENT_SECRET"Important: Old refresh tokens are invalidated immediately on rotation. Reusing an old refresh token after rotation revokes the entire authorization as a token-theft countermeasure (RFC 6749).
Request only the scopes your integration needs.
| Scope | What it allows |
|---|---|
leads:write |
Push leads into BlueClerk (creates a Ticket on the contractor's account) |
tickets:read |
Read tickets |
tickets:write |
Create and update tickets |
jobs:read |
Read jobs |
properties:read |
Read properties |
estimates:read |
Read estimates |
invoices:read |
Read invoices |
contacts:read |
Read homeowner and builder contact records |
webhooks:manage |
Create, list, and delete webhooks for the authorizing company |
All endpoints are scoped to the authorizing contractor company. You cannot access data from another company even if you have a valid token.
| Method | Path | Scope | Purpose |
|---|---|---|---|
POST |
/leads |
leads:write |
Push a lead — creates a Ticket on the contractor's account |
Request body:
{
"businessName": "Smith Plumbing",
"contactName": "Joe Smith",
"email": "joe@smithplumbing.com",
"phone": "+15125551234",
"address": "123 Main St, Austin, TX 78701",
"notes": "Needs water heater replacement",
"source": "leads2jobs"
}Response (201):
{ "ticketId": "ckxz...", "status": "OPEN" }| Method | Path | Scope |
|---|---|---|
GET |
/tickets |
tickets:read |
GET |
/tickets/:id |
tickets:read |
POST |
/tickets |
tickets:write |
PATCH |
/tickets/:id |
tickets:write |
GET /tickets query params: cursor, limit (default 25, max 100), updatedSince (ISO 8601).
| Method | Path | Scope |
|---|---|---|
GET |
/jobs |
jobs:read |
GET |
/jobs/:id |
jobs:read |
| Method | Path | Scope |
|---|---|---|
GET |
/properties |
properties:read |
GET |
/properties/:id |
properties:read |
| Method | Path | Scope |
|---|---|---|
GET |
/estimates |
estimates:read |
GET |
/invoices |
invoices:read |
GET |
/contacts |
contacts:read |
| Method | Path | Scope |
|---|---|---|
POST |
/webhooks |
webhooks:manage |
GET |
/webhooks |
webhooks:manage |
DELETE |
/webhooks/:id |
webhooks:manage |
List endpoints use cursor-based pagination. The response includes a nextCursor field; pass it back as ?cursor=... to get the next page.
curl "https://app.blueclerk.com/api/v1/jobs?limit=50&cursor=ckxz..." \
-H "Authorization: Bearer bc_at_..."Response:
{
"data": [ ... ],
"nextCursor": "ckab...",
"hasMore": true
}When hasMore is false, you've reached the end.
All list endpoints support updatedSince for incremental sync:
curl "https://app.blueclerk.com/api/v1/jobs?updatedSince=2026-05-01T00:00:00Z" \
-H "Authorization: Bearer bc_at_..."POST and PATCH endpoints accept an Idempotency-Key header. Sending the same key twice within 24 hours returns the cached response instead of creating a duplicate.
curl -X POST https://app.blueclerk.com/api/v1/leads \
-H "Authorization: Bearer bc_at_..." \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{ "businessName": "Smith Plumbing", ... }'Recommended: use a UUID for every mutating request.
Subscribe to events to get push notifications instead of polling.
curl -X POST https://app.blueclerk.com/api/v1/webhooks \
-H "Authorization: Bearer bc_at_..." \
-H "Content-Type: application/json" \
-d '{
"url": "https://yourapp.com/webhooks/blueclerk",
"events": ["job.created", "job.status_changed", "invoice.paid"]
}'Response includes a secret (returned once) — save it for signature verification.
| Event | When it fires | Payload includes |
|---|---|---|
job.created |
A new Job is created | id, title, status, propertyId, createdAt |
job.status_changed |
Job.status changes | id, oldStatus, newStatus, changedAt |
ticket.created |
A new Ticket is created | id, title, status, createdAt |
estimate.sent |
An Estimate is sent to the customer | id, estimateNumber, totalAmount, sentAt |
estimate.approved |
An Estimate is accepted | id, estimateNumber, approvedAt |
property.created |
A new Property is created | id, address, createdAt |
invoice.paid |
An Invoice transitions to fully paid | id, invoiceNumber, totalAmount, paidAt |
Every webhook delivery includes an X-BlueClerk-Signature header:
X-BlueClerk-Signature: t=1715284800,v1=abcd1234...
To verify:
- Extract
t(timestamp) andv1(HMAC-SHA256 hex digest) - Build signed payload:
{t}.{raw_request_body} - Compute HMAC-SHA256 with your webhook
secret - Compare with
v1using constant-time comparison - Reject if
tis more than 5 minutes old (replay protection)
Example verifiers in examples/webhook-verifier.js (Node) and examples/webhook-verifier.py (Python).
Failed deliveries (non-2xx response or timeout) retry with exponential backoff:
- Attempt 1: immediate
- Attempt 2: 1 minute later
- Attempt 3: 5 minutes later
- Attempt 4: 30 minutes later
- After 3 failures → marked dead, no more retries
Webhook timeout is 5 seconds. Respond with any 2xx status to acknowledge receipt.
Rate limits are per company, not per token. Multiple integrations under the same contractor share the same bucket.
| Plan | Per-minute | Per-day | Monthly cap | Webhook deliveries/day |
|---|---|---|---|---|
| Starter ($29/mo) | 30 | 5,000 | 50,000 | 2,000 |
| Pro | 60 | 15,000 | 250,000 | 7,500 |
| Business | 120 | 50,000 | 1,500,000 | 25,000 |
| Enterprise | 300 | 200,000 | unlimited | 100,000 |
When you hit a limit, the API returns:
HTTP/1.1 429 Too Many Requests
Retry-After: 42
{
"error": "rate_limit_exceeded",
"scope": "min",
"limit": 30,
"remaining": 0,
"resetAt": "2026-05-09T14:30:00Z"
}
Honor the Retry-After header.
All errors return JSON:
{ "error": "<code>", "message": "human readable" }| Status | Error code | Meaning |
|---|---|---|
401 |
invalid_token |
Bearer token missing, expired, or invalid |
403 |
insufficient_scope |
Token lacks required scope |
403 |
plan_required |
Authorizing company is on free trial — paid plan required for API access |
403 |
forbidden |
Cross-company access attempted |
404 |
not_found |
Resource doesn't exist or belongs to a different company |
409 |
conflict |
Idempotency key collision with different payload |
429 |
rate_limit_exceeded |
See Rate Limits |
500 |
server_error |
Try again with exponential backoff |
The API is available on paid plans only — Starter ($29/mo) and up. Companies on free trial cannot mint OAuth tokens; they receive 403 plan_required at the consent screen and the token endpoint.
- All timestamps are ISO 8601 UTC (
2026-05-09T14:00:00Z). - All IDs are CUIDs (
ckxz...). - All money fields are in the contractor's account currency, returned as decimal strings (
"1234.56") to avoid float precision issues. - All addresses are returned as a single string field for now (structured address coming in v2).
The full OpenAPI 3.1 specification is available at:
https://app.blueclerk.com/api/openapi.json
You can also import openapi.json in this repo into Postman, Insomnia, or any code-gen tool to generate a client SDK in your language.
Import postman/BlueClerk-API.postman_collection.json into Postman. Set environment variables:
base_url=https://app.blueclerk.com/api/v1client_id= your client IDclient_secret= your client secretaccess_token= the token you got from/oauth/token
Use the official assets in brand/ to add a "Connect to BlueClerk" button to your app:
<a href="https://app.blueclerk.com/oauth/authorize?response_type=code&client_id=YOUR_CLIENT_ID&...">
<img src="https://github.com/blueclerk/api-docs/raw/main/brand/connect-button-light.svg"
alt="Connect to BlueClerk" height="44" />
</a>Three button styles available (light, dark, pill) plus full logo variants. See brand/README.md for usage rules.
- Get API access: app.blueclerk.com/developers (self-serve)
- Bugs / questions: open an issue in this repo
- Status / outages: https://blueclerk.com/status (coming soon)