API reference · v1
Cases in. Estimates out.
Push assignments into blnd from your own systems and take the finished estimate back: the CCC-importable .xme, with a signed webhook the moment it's ready.
- REST
- JSON
- Webhooks
- OpenAPI 3.1
- POST/v1/casescase created201
- POST/v1/cases/:id/estimate12 photos queued202
- HOOKestimate.readysigned · webhook-id evt_8c1f…2xx
- GET/v1/cases/:id/files/:file_idC26006608.xme200
On this page· Introduction
Get started
Introduction
The blnd API is a small REST API over HTTPS. Requests and responses are JSON (photo uploads are multipart/form-data), every call is authenticated with a bearer key, and blnd calls you back with signed webhooks when something changes.
Every case in the workspace is visible to the API, whatever its source: portal syncs, email and the API itself. With it you can:
- Connect a claims system. Push desk-review assignments in as cases, route them to a reviewer, and get the estimate back without anyone re-keying it.
- Automate your office. Mirror blnd's inbox into your own tools, close cases when they're billed, keep your own ids in
metadata. - Bring in other sources. Create cases from a portal or a feed blnd doesn't sync itself.
Start with a test key: it works at once, only touches sandbox data, and is never billed. The OpenAPI 3.1 description covers everything on this page, for code generators and API tools.
https://blnd.one/api/v1curl https://blnd.one/api/v1{
"object": "api",
"version": "2026-10-01",
"docs": "https://blnd.one/docs/api"
}Quickstart
From a fresh key to a downloaded estimate in five calls. Everything here runs in test mode, on the free demo engine.
Make a test key
A workspace owner or admin creates keys in Settings > Developers > Keys. Pick Test, give it a name and scopes, and copy it: it's shown once. Keep it in an environment variable, then call GET /me to check it.
export BLND_API_KEY=blnd_test_…curl https://blnd.one/api/v1/me \
-H "Authorization: Bearer $BLND_API_KEY"Create a case
Send what you know about the assignment; every field is optional. Use your own id as external_id so a retry can't create a twin, and add an Idempotency-Key for safe retries. The case shows in the blnd inbox (behind the Test filter) and the answer is the case object.
curl https://blnd.one/api/v1/cases \
-H "Authorization: Bearer $BLND_API_KEY" \
-H "Idempotency-Key: a8098c1a-f86e-41d8-9d0c-3c0b8f4a1e2d" \
-H "Content-Type: application/json" \
-d '{
"external_id": "CLM-88213-01",
"claim_number": "C26006608",
"carrier": "Sedgwick",
"assignment_type": "DR",
"insured_name": "Dana Lee",
"vehicle": {
"year": 2021,
"make": "Toyota",
"model": "Camry",
"vin": "4T1G11AK5MU123456"
},
"loss": {
"type": "Collision",
"date": "2026-10-02",
"description": "Front-end, driver side"
},
"inspection_address": "12 Main St, Austin, TX 78701",
"deductible": "500",
"owner": {
"phone": "+15125550100",
"email": "dana@example.com"
},
"adjuster": {
"name": "Sam Ortiz",
"email": "sam@carrier.com"
},
"due_at": "2026-10-20T17:00:00Z",
"metadata": {
"claims_system_id": "88213"
}
}'Upload photos and start the estimate
Post 1 to 64 damage photos (JPEG, PNG or WebP, up to 15 MB each) as multipart/form-data, one photos part per file. blnd answers 202 straight away with the estimate queued, and writes it in the background.
curl https://blnd.one/api/v1/cases/7c9e6679-7425-40de-944b-e07fc1f90ae7/estimate \
-H "Authorization: Bearer $BLND_API_KEY" \
-H "Idempotency-Key: a8098c1a-f86e-41d8-9d0c-3c0b8f4a1e2d" \
-F "photos=@front.jpg;type=image/jpeg" \
-F "photos=@driver-side.jpg;type=image/jpeg" \
-F vin=4T1G11AK5MU123456Wait for estimate.ready, or poll
Add a webhook endpoint listening to estimate.ready and estimate.failed: blnd posts the estimate, files included, the moment it settles. No public URL yet? Poll GET /cases/{id}/estimate every few seconds until status is ready or failed.
{
"id": "evt_8c1f2e7d4a9b3c6e0f5a1d2b",
"object": "event",
"type": "estimate.ready",
"livemode": false,
"created_at": "2026-10-18T14:06:40.415Z",
"data": {
"object": {
"id": "e4d909c2-90d0-4fb1-a0b2-9f5e0c7a3d11",
"object": "estimate",
"case_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"status": "ready",
"files": [
{
"id": "b1e2f3a4-5c6d-4e7f-8a9b-0c1d2e3f4a5b",
"kind": "xme",
"download_url": "https://blnd.one/api/v1/cases/7c9e6679-7425-40de-944b-e07fc1f90ae7/files/b1e2f3a4-5c6d-4e7f-8a9b-0c1d2e3f4a5b"
},
{
"id": "c2f3a4b5-6d7e-4f80-9b1c-2d3e4f5a6b7c",
"kind": "xme",
"download_url": "https://blnd.one/api/v1/cases/7c9e6679-7425-40de-944b-e07fc1f90ae7/files/c2f3a4b5-6d7e-4f80-9b1c-2d3e4f5a6b7c"
}
]
}
}
}# Every few seconds, until status is "ready" or "failed"
curl https://blnd.one/api/v1/cases/7c9e6679-7425-40de-944b-e07fc1f90ae7/estimate \
-H "Authorization: Bearer $BLND_API_KEY"Download the .xme and .AWF
Each entry in the estimate's files has a download_url. Fetch it with your key (scope files:read) to get the bytes: the CCC-importable .xme and .AWF.
curl https://blnd.one/api/v1/cases/7c9e6679-7425-40de-944b-e07fc1f90ae7/files/b1e2f3a4-5c6d-4e7f-8a9b-0c1d2e3f4a5b \
-H "Authorization: Bearer $BLND_API_KEY" \
-o C26006608.xmeAuthentication
Send your key on every call as Authorization: Bearer <key>. Keys are long random strings, shown once when they're made; blnd stores only a hash. Treat them like passwords: server-side only, never in a browser or an app.
| Prefix | Mode | Works |
|---|---|---|
| blnd_test_… | Test | Once the workspace is in the Developer Program. Only sees and creates sandbox cases. |
| blnd_live_… | Live | Once the workspace is in the Developer Program. Real cases, real estimates. |
The Developer Program
The API is open to approved workspaces only. An owner or admin applies in Settings > Developers: who you are, what you're building and roughly how many cases a month. A person on the blnd team reviews every application, usually within two business days, and emails the answer. Until then no key can be created, and a key of a workspace that isn't approved gets 403 developer_program_required. Once you're in, where the workspace stands is on GET /me as api_access.
| api_access | Shown as | What it means |
|---|---|---|
| none | Not applied | Not applied yet. No key can be created and none works. |
| pending | Under review | Application submitted; a person on the blnd team is reviewing it. Keys open once it's approved. |
| approved | Approved | In the program: test and live keys work, and webhook endpoints can be enabled. |
| rejected | Not approved | Not approved. Keys don't work; you can apply again from Settings > Developers. |
| suspended | Suspended | Every key and webhook is paused, test keys too. Contact support@blnd.one. |
Scopes
Each key carries the scopes it was given, or * for full access. A call outside them is a 403 missing_scope. Give each integration only what it needs.
| Scope | Allows |
|---|---|
| cases:read | Read cases, their timeline and status |
| cases:write | Create and update cases |
| estimates:write | Upload photos and start estimates |
| files:read | Download estimate files (.xme, .AWF) |
Rolling a key
Rolling makes a new key with the same name and scopes. Choose how long the old one keeps working so a deploy can switch over without downtime: 0 h (it stops now), 1 h, 24 h or 7 days. Revoking stops a key at once.
GET /api/v1/me HTTP/1.1
Host: blnd.one
Authorization: Bearer blnd_live_…{
"error": {
"type": "authentication_error",
"code": "missing_api_key",
"message": "No API key. Send it as `Authorization: Bearer blnd_live_…`.",
"doc_url": "https://blnd.one/docs/api#errors",
"request_id": "req_5f1c0e9a7b3d2c4e6a8b0d1f"
}
}Errors
Success is a 2xx. Anything else carries one error object: a broad type, a stable code to branch on, a message written for a person (don't parse it), the param at fault when there is one, and the request_id to quote to support.
A test key never sees live cases and a live key never sees sandbox ones: an id from the other mode is a 404 resource_missing.
| HTTP | Type | Code | Meaning |
|---|---|---|---|
| 400 | invalid_request_error | parameter_missing | A required parameter wasn't sent. param names it. |
| parameter_invalid | A parameter has the wrong type or value. param names it. | ||
| invalid_json | The request body isn't valid JSON. | ||
| 401 | authentication_error | missing_api_key | No Authorization: Bearer header. |
| invalid_api_key | Not a blnd key, or the key was revoked or has expired. | ||
| 403 | permission_error | missing_scope | The key doesn't have the scope this endpoint needs. |
| developer_program_required | The key's workspace isn't approved for the Developer Program (not applied, under review, or not approved). | ||
| api_access_suspended | API access for the workspace is suspended. Test keys stop too. | ||
| 404 | invalid_request_error | resource_missing | No such object, it belongs to the other mode (test keys only see sandbox cases), or a file is missing from storage. |
| no_estimate | The case has no estimate yet. | ||
| 409 | conflict_error | case_exists | A case with this external_id already exists in the workspace (in test or live). |
| estimate_refused | The estimate can't be started for this case. | ||
| estimate_in_progress | An estimate is already running for this case. | ||
| idempotency_key_in_use | A request with this Idempotency-Key is still running. Retry shortly. | ||
| 411 | invalid_request_error | length_required | A photo upload was sent without a Content-Length header. |
| 413 | invalid_request_error | payload_too_large | A photo is over 15 MB, the upload is over 250 MB in all (25 MB with a test key), or a JSON body is over 1 MB. |
| 422 | invalid_request_error | idempotency_key_mismatch | This Idempotency-Key was used with a different request. |
| 429 | rate_limit_error | rate_limited | Too many requests this minute. Wait Retry-After seconds. |
| 500 | api_error | server_error | Something went wrong on blnd's side. Retry; quote the request_id to support. |
{
"error": {
"type": "invalid_request_error",
"code": "parameter_invalid",
"message": "vehicle.vin: must be 17 characters, A-Z and 0-9 without I, O or Q",
"param": "vehicle.vin",
"doc_url": "https://blnd.one/docs/api#errors",
"request_id": "req_5f1c0e9a7b3d2c4e6a8b0d1f"
}
}Idempotency
Network calls fail halfway. Send an Idempotency-Key header (any unique string up to 255 characters; a UUID is ideal) on POST /cases and POST /cases/{id}/estimate, and a retry can never create a second case or a second estimate.
- The first answer is kept for 24 hours and replayed for the same key, with
Idempotent-Replayed: true. Refusals (other 4xx answers) are replayed too. 409and429answers and5xxerrors are not kept: retry with the same key and it runs again.- The same key with a different body is a
422 idempotency_key_mismatch. Use a new key for a new request. - A key whose first request is still running answers
409 idempotency_key_in_use. Retry shortly.
Keys are scoped to the workspace and the mode. GET and PATCH are safe to repeat as they are.
POST /api/v1/cases HTTP/1.1
Authorization: Bearer blnd_test_…
Idempotency-Key: a8098c1a-f86e-41d8-9d0c-3c0b8f4a1e2d
HTTP/1.1 201 Created
Idempotent-Replayed: true
Request-Id: req_9e2b7c41d0a35f68be19c2d4Pagination
Lists come newest first, in pages. Pass limit (1 to 100, default 25) and, for the next page, starting_after set to the previous page's next_cursor: the id of its last object. has_more says whether another page follows; next_cursor is null on the last one.
Paged lists: cases and a case's timeline. Team members and a case's files always fit on one page. For an incremental sync, filter cases with updated_since.
curl -G https://blnd.one/api/v1/cases \
-H "Authorization: Bearer $BLND_API_KEY" \
-d limit=100 \
-d starting_after=7c9e6679-7425-40de-944b-e07fc1f90ae7{
"object": "list",
"data": [
"…"
],
"has_more": true,
"next_cursor": "7c9e6679-7425-40de-944b-e07fc1f90ae7"
}Rate limits
Each key has a budget of requests per minute: 120 for live keys and 30 for test keys, unless your workspace has its own limit. GET /me shows the one that applies as rate_limit.
Every keyed answer reports the budget in headers. Past it, the answer is 429 rate_limited with Retry-After in seconds: wait that long, then carry on.
| Header | Meaning |
|---|---|
| RateLimit-Policy | The key's quota per window, e.g. "default";q=120;w=60. |
| RateLimit | What's left and when the window resets, e.g. "default";r=119;t=60. |
| X-RateLimit-Limit | Requests allowed per minute. |
| X-RateLimit-Remaining | Requests left this minute. |
| X-RateLimit-Reset | Unix time (seconds) the window resets. |
| Retry-After | On a 429 only: seconds to wait. |
HTTP/1.1 429 Too Many Requests
Retry-After: 17
RateLimit-Policy: "default";q=120;w=60
RateLimit: "default";r=0;t=17
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1792332417Versioning
This reference describes version 2026-10-01. You don't send a version: every response says which one answered in Blnd-Version. Read objects tolerantly, ignoring fields and enum values you don't recognise.
Every response also carries a Request-Id (req_…), repeated in error bodies. Log it; quoting it lets support find the exact call.
| Response header | Meaning |
|---|---|
| Request-Id | req_…, unique to this request. Quote it to support; it's also in every error body. |
| Blnd-Version | The API version that answered: 2026-10-01. |
| RateLimit-Policy | The key's quota per window, e.g. "default";q=120;w=60. |
| RateLimit | What's left and when the window resets, e.g. "default";r=119;t=60. |
| X-RateLimit-Limit | Requests allowed per minute. |
| X-RateLimit-Remaining | Requests left this minute. |
| X-RateLimit-Reset | Unix time (seconds) the window resets. |
| Retry-After | On a 429 only: seconds to wait. |
| Idempotent-Replayed | true when the answer is a replay of an earlier request with the same Idempotency-Key. |
HTTP/1.1 200 OK
Content-Type: application/json
Request-Id: req_5f1c0e9a7b3d2c4e6a8b0d1f
Blnd-Version: 2026-10-01
Cache-Control: no-storeReference
Objects
Every object has an `id` and an `object` field naming its kind. Timestamps are ISO 8601 in UTC; fields with no value are `null`, never missing.
The case object
One assignment: a vehicle, a claim and the people around it. Cases arrive from a claims portal sync, from an email, or from your own POST /cases. Every case in a workspace is visible to the API, whatever its source.
idstringUnique identifier for the case.
objectstringAlways
case.livemodebooleantruefor real cases;falsefor sandbox cases made with a test key.sourceenumWhere the case came from.
apiportalemailportalstringnullableThe portal the case was synced from, e.g.
vista.nullfor API and email cases.external_idstringnullableYour id for an API case, or the portal's own id for a portal case.
nullfor email cases.statusenumWhere the case stands in blnd.
newin_progressdraft_readyuploadedclosedclaim_numberstringnullableThe carrier's claim number.
file_numberstringnullableThe portal's file number, when it differs from the claim number.
policy_numberstringnullableThe insured's policy number.
carrierstringnullableThe insurance carrier.
assignment_typestringnullableThe portal's or your code for the kind of work:
DR,SDR,AO,QE,TLand so on.insured_namestringnullableThe insured's name.
vehicleobjectThe vehicle.
Child attributes(4)
yearintegernullableModel year.
makestringnullableMake, e.g.
Toyota.modelstringnullableModel, e.g.
Camry.vinstringnullableThe 17-character VIN.
lossobjectWhat happened.
Child attributes(3)
typestringnullableKind of loss, e.g.
Collision.datedatenullableDate of loss,
YYYY-MM-DD.descriptionstringnullableThe loss description or point of impact.
inspection_addressstringnullableWhere the vehicle can be inspected.
deductiblestringnullableThe deductible, as written on the assignment (a string, e.g.
"500").ownerobjectHow to reach the vehicle's owner.
Child attributes(2)
phonestringnullablePhone number.
emailstringnullableEmail address.
adjusterobjectThe carrier's adjuster.
Child attributes(3)
namestringnullableName.
phonestringnullablePhone number.
emailstringnullableEmail address.
assignee_idstringnullableThe team member working the case.
GET /memberslists the ids.due_attimestampnullableWhen the work is due.
assigned_attimestampnullableWhen the case was assigned, as the portal reports it.
appointment_attimestampnullableThe inspection appointment, when one is set.
metadataobjectYour own key/value pairs. Never read by blnd.
urlstringThe case in the blnd app.
created_attimestampWhen the case was created in blnd.
updated_attimestampWhen anything on the case last changed.
{
"id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"object": "case",
"livemode": false,
"source": "api",
"portal": null,
"external_id": "CLM-88213-01",
"status": "new",
"claim_number": "C26006608",
"file_number": null,
"policy_number": null,
"carrier": "Sedgwick",
"assignment_type": "DR",
"insured_name": "Dana Lee",
"vehicle": {
"year": 2021,
"make": "Toyota",
"model": "Camry",
"vin": "4T1G11AK5MU123456"
},
"loss": {
"type": "Collision",
"date": "2026-10-02",
"description": "Front-end, driver side"
},
"inspection_address": "12 Main St, Austin, TX 78701",
"deductible": "500",
"owner": {
"phone": "+15125550100",
"email": "dana@example.com"
},
"adjuster": {
"name": "Sam Ortiz",
"phone": null,
"email": "sam@carrier.com"
},
"assignee_id": null,
"due_at": "2026-10-20T17:00:00.000Z",
"assigned_at": "2026-10-18T14:02:11.482Z",
"appointment_at": null,
"metadata": {
"claims_system_id": "88213"
},
"url": "https://blnd.one/app/cases/7c9e6679-7425-40de-944b-e07fc1f90ae7",
"created_at": "2026-10-18T14:02:11.482Z",
"updated_at": "2026-10-18T14:02:11.482Z"
}The estimate object
One estimate run for a case: from photos to a written estimate file. A new estimate replaces the case's previous one. Poll it with GET /cases/{id}/estimate or listen for the estimate.* webhooks.
idstringUnique identifier for the estimate.
objectstringAlways
estimate.case_idstringThe case this estimate belongs to.
statusenumWhere the estimate stands. Branch on this field.
awaiting_photosqueuedprocessingreadyfailedstageenumThe finer-grained internal step behind
status. Useful for display; may gain values.awaiting_photosqueueddraftingpricingbuildingreadyfailedstepstringnullableProgress in words while it runs, e.g.
Reading the photos.errorstringnullableWhy the estimate failed, when
statusisfailed.photo_countintegerHow many photos the estimate was started with.
vinstringnullableThe VIN the estimate was written for.
totalsobjectnullableThe estimate's money totals, once it is
ready.nulluntil then. Keys inside are camelCase.Child attributes(7)
categoriesarray of objectsOne row per category (parts, labor kinds, materials).
Child attributes(4)
labelstringCategory name, e.g.
Body Labor.hoursnumberoptionalLabor hours, for labor categories.
ratenumberoptionalHourly rate, for labor categories.
costnumberThe category's total, in dollars.
subtotalnumberBefore tax.
salesTaxnumberSales tax, in dollars.
taxRatenumberSales tax rate, as a percentage (e.g.
8.25).grandTotalnumberThe estimate's total, tax included.
netTotalnumbergrandTotalless the deductible.deductiblenumberThe deductible applied.
line_countintegernullableNumber of estimate lines, once
ready.notesarray of stringPlain-language notes about the run, e.g. a file that could not be built.
filesarray of fileThe files the estimate produced (.xme, .AWF). Empty until
ready.started_attimestampWhen the estimate was started.
finished_attimestampnullableWhen it became
readyorfailed.
{
"id": "e4d909c2-90d0-4fb1-a0b2-9f5e0c7a3d11",
"object": "estimate",
"case_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"status": "ready",
"stage": "ready",
"step": null,
"error": null,
"photo_count": 12,
"vin": "4T1G11AK5MU123456",
"totals": {
"categories": [
{
"label": "Parts",
"cost": 1842.5
},
{
"label": "Body Labor",
"hours": 7.4,
"rate": 58,
"cost": 429.2
},
{
"label": "Paint Labor",
"hours": 4.8,
"rate": 58,
"cost": 278.4
},
{
"label": "Paint Supplies",
"hours": 4.8,
"rate": 42,
"cost": 201.6
}
],
"subtotal": 2751.7,
"salesTax": 227.02,
"taxRate": 8.25,
"grandTotal": 2978.72,
"netTotal": 2478.72,
"deductible": 500
},
"line_count": 14,
"notes": [],
"files": [
{
"id": "b1e2f3a4-5c6d-4e7f-8a9b-0c1d2e3f4a5b",
"object": "file",
"case_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"kind": "xme",
"name": "C26006608.xme",
"bytes": 61440,
"uploaded_to_portal": false,
"download_url": "https://blnd.one/api/v1/cases/7c9e6679-7425-40de-944b-e07fc1f90ae7/files/b1e2f3a4-5c6d-4e7f-8a9b-0c1d2e3f4a5b",
"created_at": "2026-10-18T14:06:40.117Z"
},
{
"id": "c2f3a4b5-6d7e-4f80-9b1c-2d3e4f5a6b7c",
"object": "file",
"case_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"kind": "xme",
"name": "C26006608.xme",
"bytes": 41872,
"uploaded_to_portal": false,
"download_url": "https://blnd.one/api/v1/cases/7c9e6679-7425-40de-944b-e07fc1f90ae7/files/c2f3a4b5-6d7e-4f80-9b1c-2d3e4f5a6b7c",
"created_at": "2026-10-18T14:06:40.117Z"
}
],
"started_at": "2026-10-18T14:03:02.950Z",
"finished_at": "2026-10-18T14:06:40.402Z"
}The file object
A file blnd wrote for a case: the CCC-importable .xme or .AWF.
idstringUnique identifier for the file.
objectstringAlways
file.case_idstringThe case the file belongs to.
kindenumThe file type.
xmeawfnamestringThe file name.
bytesintegernullableSize in bytes.
uploaded_to_portalbooleanWhether blnd has uploaded this file back to the case's portal.
download_urlstringDownload the bytes with your API key (
files:read).created_attimestampWhen the file was written.
{
"id": "b1e2f3a4-5c6d-4e7f-8a9b-0c1d2e3f4a5b",
"object": "file",
"case_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"kind": "xme",
"name": "C26006608.xme",
"bytes": 61440,
"uploaded_to_portal": false,
"download_url": "https://blnd.one/api/v1/cases/7c9e6679-7425-40de-944b-e07fc1f90ae7/files/b1e2f3a4-5c6d-4e7f-8a9b-0c1d2e3f4a5b",
"created_at": "2026-10-18T14:06:40.117Z"
}The case event object
One entry on a case's timeline: a sync, a status change, a file written or uploaded, a note.
idstringUnique identifier. Use it as
starting_afterto page.objectstringAlways
case_event.case_idstringThe case the entry belongs to.
typestringThe kind of entry, e.g.
synced,status,file,upload,note. Treat unknown types as plain entries.messagestringWhat happened, in words.
attimestampWhen it happened.
{
"id": "0f8fad5b-d9cb-469f-a165-70867728950e",
"object": "case_event",
"case_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"type": "file",
"message": "Estimate written: 14 lines, $2,978.72. .xme, .AWF ready",
"at": "2026-10-18T14:06:40.311Z"
}The event object
What a webhook delivers. data.object is the case or estimate as it was when the event happened.
idstringUnique identifier,
evt_…. The same as thewebhook-idheader and stable across retries: dedupe on it.objectstringAlways
event.typeenumThe event type.
case.createdcase.updatedestimate.startedestimate.readyestimate.failedlivemodebooleanfalsefor events about sandbox data.created_attimestampWhen the event happened.
dataobjectThe event's subject.
Child attributes(1)
objectcase | estimateA case for
case.*events, an estimate forestimate.*events.
{
"id": "evt_8c1f2e7d4a9b3c6e0f5a1d2b",
"object": "event",
"type": "estimate.ready",
"livemode": false,
"created_at": "2026-10-18T14:06:40.415Z",
"data": {
"object": {
"id": "e4d909c2-90d0-4fb1-a0b2-9f5e0c7a3d11",
"object": "estimate",
"case_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"status": "ready",
"stage": "ready",
"step": null,
"error": null,
"photo_count": 12,
"vin": "4T1G11AK5MU123456",
"totals": {
"categories": [
{
"label": "Parts",
"cost": 1842.5
},
{
"label": "Body Labor",
"hours": 7.4,
"rate": 58,
"cost": 429.2
},
{
"label": "Paint Labor",
"hours": 4.8,
"rate": 58,
"cost": 278.4
},
{
"label": "Paint Supplies",
"hours": 4.8,
"rate": 42,
"cost": 201.6
}
],
"subtotal": 2751.7,
"salesTax": 227.02,
"taxRate": 8.25,
"grandTotal": 2978.72,
"netTotal": 2478.72,
"deductible": 500
},
"line_count": 14,
"notes": [],
"files": [
{
"id": "b1e2f3a4-5c6d-4e7f-8a9b-0c1d2e3f4a5b",
"object": "file",
"case_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"kind": "xme",
"name": "C26006608.xme",
"bytes": 61440,
"uploaded_to_portal": false,
"download_url": "https://blnd.one/api/v1/cases/7c9e6679-7425-40de-944b-e07fc1f90ae7/files/b1e2f3a4-5c6d-4e7f-8a9b-0c1d2e3f4a5b",
"created_at": "2026-10-18T14:06:40.117Z"
},
{
"id": "c2f3a4b5-6d7e-4f80-9b1c-2d3e4f5a6b7c",
"object": "file",
"case_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"kind": "xme",
"name": "C26006608.xme",
"bytes": 41872,
"uploaded_to_portal": false,
"download_url": "https://blnd.one/api/v1/cases/7c9e6679-7425-40de-944b-e07fc1f90ae7/files/c2f3a4b5-6d7e-4f80-9b1c-2d3e4f5a6b7c",
"created_at": "2026-10-18T14:06:40.117Z"
}
],
"started_at": "2026-10-18T14:03:02.950Z",
"finished_at": "2026-10-18T14:06:40.402Z"
}
}
}Endpoints
Workspace
Check the API, the key and the team. Besides each endpoint's own errors, any call with a key can answer `401`, `403`, `429` or `500`.
/v1Check the API
Answers without a key: the API version and where the docs live. Handy as a health check.
Returns
200 The API object.
objectstringAlways
api.versionstringThe API version, e.g.
2026-10-01.docsstringWhere these docs live.
Full URL: https://blnd.one/api/v1
curl https://blnd.one/api/v1{
"object": "api",
"version": "2026-10-01",
"docs": "https://blnd.one/docs/api"
}/v1/meRetrieve your workspace
The workspace and the key behind this request: its mode, scopes and rate limit, and where the workspace's Developer Program status stands. The first call to make with a new key.
Returns
200 The workspace object.
objectstringAlways
workspace.idstringThe workspace id.
namestringnullableThe workspace name.
livemodebooleantruefor a live key,falsefor a test key.api_accessenumWhere the workspace's live API access stands.
nonependingapprovedrejectedsuspendedkeyobjectThe key making this request.
Child attributes(4)
idstringThe key id.
namestringnullableThe name you gave the key.
modeenumLive or test.
livetestscopesarray of stringWhat the key may do;
["*"]is full access.
rate_limitintegerRequests per minute this key may make.
Full URL: https://blnd.one/api/v1/me
curl https://blnd.one/api/v1/me \
-H "Authorization: Bearer $BLND_API_KEY"{
"object": "workspace",
"id": "org_2kq8Zt0VbN4mXc7Ra1Lp",
"name": "Lone Star Desk Review",
"livemode": false,
"api_access": "pending",
"key": {
"id": "key_9d2c71",
"name": "Claims system (staging)",
"mode": "test",
"scopes": [
"*"
]
},
"rate_limit": 30
}/v1/membersList team members
The people on the workspace's team. Their ids are what assignee_id takes when you route a case to a reviewer.
cases:readReturns
200 A list of member objects.
idstringThe person's user id: the value
assignee_idtakes and cases carry.objectstringAlways
member.namestringDisplay name.
emailstringEmail address.
rolestringTheir role in the workspace, e.g.
owner,admin,member.
Full URL: https://blnd.one/api/v1/members
curl https://blnd.one/api/v1/members \
-H "Authorization: Bearer $BLND_API_KEY"{
"object": "list",
"data": [
{
"id": "Hk3pQ8vLr2NwT6yZb1Xc",
"object": "member",
"name": "Riley Chen",
"email": "riley@deskreview.example",
"role": "admin"
}
],
"has_more": false,
"next_cursor": null
}Endpoints
Cases
Assignments from every source. Test keys see sandbox cases only; live keys, live cases only.
/v1/casesList cases
Every case the key can see, newest first, from every source: portal syncs, email and the API. Filters combine.
cases:readPaginatedQuery parameters
limitintegerHow many objects to return, 1 to 100. Default 25.
starting_afterstringA cursor: the
idof the last object on the previous page (next_cursor).statusenumOnly cases with this status.
newin_progressdraft_readyuploadedclosedsourceenumOnly cases from this source.
apiportalemailportalstringOnly cases synced from this portal, e.g.
vista.assignee_idstringOnly cases routed to this team member.
claim_numberstringOnly cases with this claim number.
updated_sincetimestampOnly cases changed at or after this ISO 8601 time. Good for an incremental sync.
Returns
200 A list of case objects.
Notable errors
| Status | Code | When |
|---|---|---|
| 400 | parameter_invalid | A filter or limit is out of range, or starting_after isn't in this list. |
Full URL: https://blnd.one/api/v1/cases
curl -G https://blnd.one/api/v1/cases \
-H "Authorization: Bearer $BLND_API_KEY" \
-d status=new \
-d limit=10{
"object": "list",
"data": [
{
"id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"object": "case",
"livemode": false,
"source": "api",
"portal": null,
"external_id": "CLM-88213-01",
"status": "new",
"claim_number": "C26006608",
"file_number": null,
"policy_number": null,
"carrier": "Sedgwick",
"assignment_type": "DR",
"insured_name": "Dana Lee",
"vehicle": {
"year": 2021,
"make": "Toyota",
"model": "Camry",
"vin": "4T1G11AK5MU123456"
},
"loss": {
"type": "Collision",
"date": "2026-10-02",
"description": "Front-end, driver side"
},
"inspection_address": "12 Main St, Austin, TX 78701",
"deductible": "500",
"owner": {
"phone": "+15125550100",
"email": "dana@example.com"
},
"adjuster": {
"name": "Sam Ortiz",
"phone": null,
"email": "sam@carrier.com"
},
"assignee_id": null,
"due_at": "2026-10-20T17:00:00.000Z",
"assigned_at": "2026-10-18T14:02:11.482Z",
"appointment_at": null,
"metadata": {
"claims_system_id": "88213"
},
"url": "https://blnd.one/app/cases/7c9e6679-7425-40de-944b-e07fc1f90ae7",
"created_at": "2026-10-18T14:02:11.482Z",
"updated_at": "2026-10-18T14:02:11.482Z"
}
],
"has_more": true,
"next_cursor": "7c9e6679-7425-40de-944b-e07fc1f90ae7"
}/v1/casesCreate a case
Push an assignment into blnd: from your claims system, a desk-review queue, or a portal blnd doesn't cover. Every field is optional; send what you know. The case shows in the blnd inbox at once and fires case.created.
cases:writeAccepts Idempotency-KeyBody · application/json
external_idstringYour id for the case. Unique per workspace, across test and live: a second create with the same id, in either mode, is a
409 case_exists. Leave it out and the case has none.1 to 255 characters; can't start with
blnd:ormail:.claim_numberstringnullableThe carrier's claim number.
At most 500 characters.
file_numberstringnullableYour or the portal's file number.
At most 500 characters.
policy_numberstringnullableThe insured's policy number.
At most 500 characters.
carrierstringnullableThe insurance carrier.
At most 500 characters.
assignment_typestringnullableYour code for the kind of work, e.g.
DRfor desk review.At most 20 characters.
insured_namestringnullableThe insured's name.
At most 500 characters.
vehicleobjectThe vehicle. Every key is optional.
Child attributes(4)
yearintegernullableModel year.
1900 to 2100.
makestringnullableMake.
At most 500 characters.
modelstringnullableModel.
At most 500 characters.
vinstringnullableThe VIN. Upper-cased for you.
17 characters, A-Z and 0-9 without I, O or Q.
lossobjectWhat happened. Every key is optional.
Child attributes(3)
typestringnullableKind of loss, e.g.
Collision.At most 500 characters.
datedatenullableDate of loss.
YYYY-MM-DD.descriptionstringnullableThe loss description or point of impact.
At most 5000 characters.
inspection_addressstringnullableWhere the vehicle can be inspected.
At most 500 characters.
deductiblestring | numbernullableThe deductible. A number is accepted and stored as a string.
At most 50 characters.
ownerobjectThe vehicle owner's contact details.
Child attributes(2)
phonestringnullablePhone number.
At most 50 characters.
emailstringnullableEmail address.
At most 320 characters.
adjusterobjectThe carrier's adjuster.
Child attributes(3)
namestringnullableName.
At most 500 characters.
phonestringnullablePhone number.
At most 50 characters.
emailstringnullableEmail address.
At most 320 characters.
assignee_idstringnullableRoute the case to a team member: their user id, from
GET /members.due_attimestampnullableWhen the work is due. ISO 8601 with a time zone offset.
metadataobjectnullableYour own key/value pairs. Keys merge into what's there; a
nullvalue removes that key;nullfor the whole object clears it.At most 20 keys of up to 40 characters; string values up to 500 characters.
Returns
201 The case object.
Notable errors
| Status | Code | When |
|---|---|---|
| 400 | parameter_invalid | A field fails validation, an unknown field was sent, or assignee_id isn't a team member; param says which. |
| 409 | case_exists | external_id is already taken in this workspace, in test or live. |
Full URL: https://blnd.one/api/v1/cases
curl https://blnd.one/api/v1/cases \
-H "Authorization: Bearer $BLND_API_KEY" \
-H "Idempotency-Key: a8098c1a-f86e-41d8-9d0c-3c0b8f4a1e2d" \
-H "Content-Type: application/json" \
-d '{
"external_id": "CLM-88213-01",
"claim_number": "C26006608",
"carrier": "Sedgwick",
"assignment_type": "DR",
"insured_name": "Dana Lee",
"vehicle": {
"year": 2021,
"make": "Toyota",
"model": "Camry",
"vin": "4T1G11AK5MU123456"
},
"loss": {
"type": "Collision",
"date": "2026-10-02",
"description": "Front-end, driver side"
},
"inspection_address": "12 Main St, Austin, TX 78701",
"deductible": "500",
"owner": {
"phone": "+15125550100",
"email": "dana@example.com"
},
"adjuster": {
"name": "Sam Ortiz",
"email": "sam@carrier.com"
},
"due_at": "2026-10-20T17:00:00Z",
"metadata": {
"claims_system_id": "88213"
}
}'{
"id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"object": "case",
"livemode": false,
"source": "api",
"portal": null,
"external_id": "CLM-88213-01",
"status": "new",
"claim_number": "C26006608",
"file_number": null,
"policy_number": null,
"carrier": "Sedgwick",
"assignment_type": "DR",
"insured_name": "Dana Lee",
"vehicle": {
"year": 2021,
"make": "Toyota",
"model": "Camry",
"vin": "4T1G11AK5MU123456"
},
"loss": {
"type": "Collision",
"date": "2026-10-02",
"description": "Front-end, driver side"
},
"inspection_address": "12 Main St, Austin, TX 78701",
"deductible": "500",
"owner": {
"phone": "+15125550100",
"email": "dana@example.com"
},
"adjuster": {
"name": "Sam Ortiz",
"phone": null,
"email": "sam@carrier.com"
},
"assignee_id": null,
"due_at": "2026-10-20T17:00:00.000Z",
"assigned_at": "2026-10-18T14:02:11.482Z",
"appointment_at": null,
"metadata": {
"claims_system_id": "88213"
},
"url": "https://blnd.one/app/cases/7c9e6679-7425-40de-944b-e07fc1f90ae7",
"created_at": "2026-10-18T14:02:11.482Z",
"updated_at": "2026-10-18T14:02:11.482Z"
}/v1/cases/{id}Retrieve a case
One case by its id.
cases:readPath parameters
idstringRequiredThe case id.
Returns
200 The case object.
Notable errors
| Status | Code | When |
|---|---|---|
| 404 | resource_missing | No such case in this mode. |
Full URL: https://blnd.one/api/v1/cases/{id}
curl https://blnd.one/api/v1/cases/7c9e6679-7425-40de-944b-e07fc1f90ae7 \
-H "Authorization: Bearer $BLND_API_KEY"{
"id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"object": "case",
"livemode": false,
"source": "api",
"portal": null,
"external_id": "CLM-88213-01",
"status": "new",
"claim_number": "C26006608",
"file_number": null,
"policy_number": null,
"carrier": "Sedgwick",
"assignment_type": "DR",
"insured_name": "Dana Lee",
"vehicle": {
"year": 2021,
"make": "Toyota",
"model": "Camry",
"vin": "4T1G11AK5MU123456"
},
"loss": {
"type": "Collision",
"date": "2026-10-02",
"description": "Front-end, driver side"
},
"inspection_address": "12 Main St, Austin, TX 78701",
"deductible": "500",
"owner": {
"phone": "+15125550100",
"email": "dana@example.com"
},
"adjuster": {
"name": "Sam Ortiz",
"phone": null,
"email": "sam@carrier.com"
},
"assignee_id": null,
"due_at": "2026-10-20T17:00:00.000Z",
"assigned_at": "2026-10-18T14:02:11.482Z",
"appointment_at": null,
"metadata": {
"claims_system_id": "88213"
},
"url": "https://blnd.one/app/cases/7c9e6679-7425-40de-944b-e07fc1f90ae7",
"created_at": "2026-10-18T14:02:11.482Z",
"updated_at": "2026-10-18T14:02:11.482Z"
}/v1/cases/{id}Update a case
Change what you send; everything else stays. API cases take any create field. Portal and email cases belong to their source, so only assignee_id, assignment_type and metadata change on them. Any case can be closed with status: "closed" (not while an estimate runs) and reopened with status: "new". Fires case.updated.
cases:writePath parameters
idstringRequiredThe case id.
Body · application/json
statusenumclosedcloses the case (not while an estimate is running);newreopens a closed case.closednewexternal_idstringYour id for the case. Unique per workspace, across test and live: a second create with the same id, in either mode, is a
409 case_exists. Leave it out and the case has none.1 to 255 characters; can't start with
blnd:ormail:.claim_numberstringnullableThe carrier's claim number.
At most 500 characters.
file_numberstringnullableYour or the portal's file number.
At most 500 characters.
policy_numberstringnullableThe insured's policy number.
At most 500 characters.
carrierstringnullableThe insurance carrier.
At most 500 characters.
assignment_typestringnullableYour code for the kind of work, e.g.
DRfor desk review.At most 20 characters.
insured_namestringnullableThe insured's name.
At most 500 characters.
vehicleobjectThe vehicle. Every key is optional.
Child attributes(4)
yearintegernullableModel year.
1900 to 2100.
makestringnullableMake.
At most 500 characters.
modelstringnullableModel.
At most 500 characters.
vinstringnullableThe VIN. Upper-cased for you.
17 characters, A-Z and 0-9 without I, O or Q.
lossobjectWhat happened. Every key is optional.
Child attributes(3)
typestringnullableKind of loss, e.g.
Collision.At most 500 characters.
datedatenullableDate of loss.
YYYY-MM-DD.descriptionstringnullableThe loss description or point of impact.
At most 5000 characters.
inspection_addressstringnullableWhere the vehicle can be inspected.
At most 500 characters.
deductiblestring | numbernullableThe deductible. A number is accepted and stored as a string.
At most 50 characters.
ownerobjectThe vehicle owner's contact details.
Child attributes(2)
phonestringnullablePhone number.
At most 50 characters.
emailstringnullableEmail address.
At most 320 characters.
adjusterobjectThe carrier's adjuster.
Child attributes(3)
namestringnullableName.
At most 500 characters.
phonestringnullablePhone number.
At most 50 characters.
emailstringnullableEmail address.
At most 320 characters.
assignee_idstringnullableRoute the case to a team member: their user id, from
GET /members.due_attimestampnullableWhen the work is due. ISO 8601 with a time zone offset.
metadataobjectnullableYour own key/value pairs. Keys merge into what's there; a
nullvalue removes that key;nullfor the whole object clears it.At most 20 keys of up to 40 characters; string values up to 500 characters.
Returns
200 The case object.
Notable errors
| Status | Code | When |
|---|---|---|
| 400 | parameter_invalid | A field fails validation, the field can't change on a portal or email case, assignee_id isn't a team member, or status: "new" on a case that isn't closed. |
| 404 | resource_missing | No such case in this mode. |
| 409 | estimate_in_progress | Closing a case while its estimate runs. |
| 409 | case_exists | A new external_id that another case already has. |
Full URL: https://blnd.one/api/v1/cases/{id}
curl -X PATCH https://blnd.one/api/v1/cases/7c9e6679-7425-40de-944b-e07fc1f90ae7 \
-H "Authorization: Bearer $BLND_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"assignee_id": "Hk3pQ8vLr2NwT6yZb1Xc",
"metadata": {
"queue": "desk-review"
}
}'{
"id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"object": "case",
"livemode": false,
"source": "api",
"portal": null,
"external_id": "CLM-88213-01",
"status": "new",
"claim_number": "C26006608",
"file_number": null,
"policy_number": null,
"carrier": "Sedgwick",
"assignment_type": "DR",
"insured_name": "Dana Lee",
"vehicle": {
"year": 2021,
"make": "Toyota",
"model": "Camry",
"vin": "4T1G11AK5MU123456"
},
"loss": {
"type": "Collision",
"date": "2026-10-02",
"description": "Front-end, driver side"
},
"inspection_address": "12 Main St, Austin, TX 78701",
"deductible": "500",
"owner": {
"phone": "+15125550100",
"email": "dana@example.com"
},
"adjuster": {
"name": "Sam Ortiz",
"phone": null,
"email": "sam@carrier.com"
},
"assignee_id": "Hk3pQ8vLr2NwT6yZb1Xc",
"due_at": "2026-10-20T17:00:00.000Z",
"assigned_at": "2026-10-18T14:02:11.482Z",
"appointment_at": null,
"metadata": {
"claims_system_id": "88213",
"queue": "desk-review"
},
"url": "https://blnd.one/app/cases/7c9e6679-7425-40de-944b-e07fc1f90ae7",
"created_at": "2026-10-18T14:02:11.482Z",
"updated_at": "2026-10-18T14:02:58.006Z"
}/v1/cases/{id}/eventsList a case's timeline
The case's timeline, newest first: syncs, status changes, files written and uploaded, notes.
cases:readPaginatedPath parameters
idstringRequiredThe case id.
Query parameters
limitintegerHow many objects to return, 1 to 100. Default 25.
starting_afterstringA cursor: the
idof the last object on the previous page (next_cursor).
Returns
200 A list of case event objects.
Notable errors
| Status | Code | When |
|---|---|---|
| 404 | resource_missing | No such case in this mode. |
Full URL: https://blnd.one/api/v1/cases/{id}/events
curl https://blnd.one/api/v1/cases/7c9e6679-7425-40de-944b-e07fc1f90ae7/events \
-H "Authorization: Bearer $BLND_API_KEY"{
"object": "list",
"data": [
{
"id": "0f8fad5b-d9cb-469f-a165-70867728950e",
"object": "case_event",
"case_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"type": "file",
"message": "Estimate written: 14 lines, $2,978.72. .xme, .AWF ready",
"at": "2026-10-18T14:06:40.311Z"
},
{
"id": "1b4e28ba-2fa1-41d2-883f-0016d3cca427",
"object": "case_event",
"case_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"type": "note",
"message": "Created through the API",
"at": "2026-10-18T14:02:11.502Z"
}
],
"has_more": false,
"next_cursor": null
}Endpoints
Estimates
Photos in, a written estimate out. One estimate runs per case at a time; a new one replaces the last.
/v1/cases/{id}/estimateStart an estimate
Upload the damage photos and start writing the estimate. Answers 202 Accepted at once with the estimate in queued; follow it with estimate.ready / estimate.failed webhooks or by polling. With a test key the free demo engine writes it.
estimates:writeAccepts Idempotency-KeyPath parameters
idstringRequiredThe case id.
Form fields · multipart/form-data
photosarray of filesRequiredThe damage photos. Repeat the
photosfield once per file.1 to 64 files; JPEG, PNG or WebP; at most 15 MB each. Send the body as
multipart/form-data.vinstringnullableThe VIN to write the estimate for. Defaults to the case's VIN.
17 characters.
Returns
202 The estimate object. Work continues in the background.
Notable errors
| Status | Code | When |
|---|---|---|
| 400 | parameter_missing | No photos were sent. |
| 400 | parameter_invalid | The body isn't multipart/form-data, a photo isn't JPEG, PNG or WebP, there are more than 64, or vin isn't a valid VIN. |
| 409 | estimate_in_progress | An estimate is already running for this case. |
| 409 | estimate_refused | The estimate can't be started for this case. |
| 411 | length_required | The upload has no Content-Length header (chunked uploads aren't accepted). |
| 413 | payload_too_large | A photo is over 15 MB, or the upload is over 250 MB in all (25 MB with a test key). |
Full URL: https://blnd.one/api/v1/cases/{id}/estimate
curl https://blnd.one/api/v1/cases/7c9e6679-7425-40de-944b-e07fc1f90ae7/estimate \
-H "Authorization: Bearer $BLND_API_KEY" \
-H "Idempotency-Key: a8098c1a-f86e-41d8-9d0c-3c0b8f4a1e2d" \
-F "photos=@front.jpg;type=image/jpeg" \
-F "photos=@driver-side.jpg;type=image/jpeg" \
-F vin=4T1G11AK5MU123456{
"id": "e4d909c2-90d0-4fb1-a0b2-9f5e0c7a3d11",
"object": "estimate",
"case_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"status": "queued",
"stage": "queued",
"step": null,
"error": null,
"photo_count": 12,
"vin": "4T1G11AK5MU123456",
"totals": null,
"line_count": null,
"notes": [],
"files": [],
"started_at": "2026-10-18T14:03:02.950Z",
"finished_at": null
}/v1/cases/{id}/estimateRetrieve the estimate
The case's newest estimate. Polling also moves a running estimate along, so a poll every few seconds is a fine alternative to webhooks.
cases:readPath parameters
idstringRequiredThe case id.
Returns
200 The estimate object.
Notable errors
| Status | Code | When |
|---|---|---|
| 404 | resource_missing | No such case in this mode. |
| 404 | no_estimate | No estimate has been started for this case. |
Full URL: https://blnd.one/api/v1/cases/{id}/estimate
curl https://blnd.one/api/v1/cases/7c9e6679-7425-40de-944b-e07fc1f90ae7/estimate \
-H "Authorization: Bearer $BLND_API_KEY"{
"id": "e4d909c2-90d0-4fb1-a0b2-9f5e0c7a3d11",
"object": "estimate",
"case_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"status": "ready",
"stage": "ready",
"step": null,
"error": null,
"photo_count": 12,
"vin": "4T1G11AK5MU123456",
"totals": {
"categories": [
{
"label": "Parts",
"cost": 1842.5
},
{
"label": "Body Labor",
"hours": 7.4,
"rate": 58,
"cost": 429.2
},
{
"label": "Paint Labor",
"hours": 4.8,
"rate": 58,
"cost": 278.4
},
{
"label": "Paint Supplies",
"hours": 4.8,
"rate": 42,
"cost": 201.6
}
],
"subtotal": 2751.7,
"salesTax": 227.02,
"taxRate": 8.25,
"grandTotal": 2978.72,
"netTotal": 2478.72,
"deductible": 500
},
"line_count": 14,
"notes": [],
"files": [
{
"id": "b1e2f3a4-5c6d-4e7f-8a9b-0c1d2e3f4a5b",
"object": "file",
"case_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"kind": "xme",
"name": "C26006608.xme",
"bytes": 61440,
"uploaded_to_portal": false,
"download_url": "https://blnd.one/api/v1/cases/7c9e6679-7425-40de-944b-e07fc1f90ae7/files/b1e2f3a4-5c6d-4e7f-8a9b-0c1d2e3f4a5b",
"created_at": "2026-10-18T14:06:40.117Z"
},
{
"id": "c2f3a4b5-6d7e-4f80-9b1c-2d3e4f5a6b7c",
"object": "file",
"case_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"kind": "xme",
"name": "C26006608.xme",
"bytes": 41872,
"uploaded_to_portal": false,
"download_url": "https://blnd.one/api/v1/cases/7c9e6679-7425-40de-944b-e07fc1f90ae7/files/c2f3a4b5-6d7e-4f80-9b1c-2d3e4f5a6b7c",
"created_at": "2026-10-18T14:06:40.117Z"
}
],
"started_at": "2026-10-18T14:03:02.950Z",
"finished_at": "2026-10-18T14:06:40.402Z"
}Endpoints
Files
The CCC-importable .xme and .AWF, as files you download with your key.
/v1/cases/{id}/filesList a case's files
The estimate files blnd wrote for the case: the CCC-importable .xme and .AWF.
files:readPath parameters
idstringRequiredThe case id.
Returns
200 A list of file objects.
Notable errors
| Status | Code | When |
|---|---|---|
| 404 | resource_missing | No such case in this mode. |
Full URL: https://blnd.one/api/v1/cases/{id}/files
curl https://blnd.one/api/v1/cases/7c9e6679-7425-40de-944b-e07fc1f90ae7/files \
-H "Authorization: Bearer $BLND_API_KEY"{
"object": "list",
"data": [
{
"id": "b1e2f3a4-5c6d-4e7f-8a9b-0c1d2e3f4a5b",
"object": "file",
"case_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"kind": "xme",
"name": "C26006608.xme",
"bytes": 61440,
"uploaded_to_portal": false,
"download_url": "https://blnd.one/api/v1/cases/7c9e6679-7425-40de-944b-e07fc1f90ae7/files/b1e2f3a4-5c6d-4e7f-8a9b-0c1d2e3f4a5b",
"created_at": "2026-10-18T14:06:40.117Z"
},
{
"id": "c2f3a4b5-6d7e-4f80-9b1c-2d3e4f5a6b7c",
"object": "file",
"case_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"kind": "xme",
"name": "C26006608.xme",
"bytes": 41872,
"uploaded_to_portal": false,
"download_url": "https://blnd.one/api/v1/cases/7c9e6679-7425-40de-944b-e07fc1f90ae7/files/c2f3a4b5-6d7e-4f80-9b1c-2d3e4f5a6b7c",
"created_at": "2026-10-18T14:06:40.117Z"
}
],
"has_more": false,
"next_cursor": null
}/v1/cases/{id}/files/{file_id}Download a file
The file's bytes (application/octet-stream), with Content-Disposition: attachment and the file name. Follow download_url on a file object with your key.
files:readPath parameters
idstringRequiredThe case id.
file_idstringRequiredThe file id.
Returns
200 The file's bytes.
Notable errors
| Status | Code | When |
|---|---|---|
| 404 | resource_missing | No such case or file in this mode, or the file is missing from storage (start the estimate again). |
Full URL: https://blnd.one/api/v1/cases/{id}/files/{file_id}
curl https://blnd.one/api/v1/cases/7c9e6679-7425-40de-944b-e07fc1f90ae7/files/b1e2f3a4-5c6d-4e7f-8a9b-0c1d2e3f4a5b \
-H "Authorization: Bearer $BLND_API_KEY" \
-o C26006608.xme// 200 OK: the file's bytes
// Content-Disposition: attachment; filename="C26006608.xme"Webhooks
Webhooks
blnd posts an event to your HTTPS endpoint when a case arrives or changes and when an estimate starts, finishes or fails. Signed per Standard Webhooks.
Setting up an endpoint
Add endpoints in Settings > Developers > Webhooks. Each one has:
- An https:// URL on a public host. Credentials go in your receiver, not in the URL.
- A mode: a test endpoint hears only sandbox data, a live one only live data. Like keys, endpoints need an approved Developer Program workspace.
- The events it listens to, or all of them.
- A signing secret,
whsec_…, to verify that a delivery came from blnd.
Send test event on the endpoint's page posts a signed sample, and every delivery's status, answer and timing is listed there, with a Resend button.
The body is an event object: { id, object: "event", type, livemode, created_at, data: { object } }, where data.object is the case or estimate as it was at that moment.
POST /webhooks/blnd HTTP/1.1
Content-Type: application/json
User-Agent: blnd-webhooks/1
webhook-id: evt_8c1f2e7d4a9b3c6e0f5a1d2b
webhook-timestamp: 1792332400
webhook-signature: v1,f20wpH3s+o397OLg8QQfHSnD3so2AdU0s4cIZqgOU4c=
{"id":"evt_8c1f2e7d4a9b3c6e0f5a1d2b","object":"event","type":"case.created","livemode":false,"created_at":"2026-10-18T14:06:40.000Z","data":{"object":{"id":"7c9e6679-7425-40de-944b-e07fc1f90ae7","object":"case"}}}Event types
| Event | data.object | When |
|---|---|---|
| case.created | case | A case arrived: from a portal sync, an email, or the API |
| case.updated | case | A case's details, status or assignee changed |
| estimate.started | estimate | An estimate began drafting |
| estimate.ready | estimate | The estimate files are written and ready to download |
| estimate.failed | estimate | An estimate stopped; the error says why |
estimate.ready carries the estimate with its files, so a receiver can download them straight away. New event types may be added; ignore the ones you don't handle.
{
"id": "evt_8c1f2e7d4a9b3c6e0f5a1d2b",
"object": "event",
"type": "estimate.ready",
"livemode": false,
"created_at": "2026-10-18T14:06:40.415Z",
"data": {
"object": {
"id": "e4d909c2-90d0-4fb1-a0b2-9f5e0c7a3d11",
"object": "estimate",
"case_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"status": "ready",
"stage": "ready",
"step": null,
"error": null,
"photo_count": 12,
"vin": "4T1G11AK5MU123456",
"totals": {
"categories": [
{
"label": "Parts",
"cost": 1842.5
},
{
"label": "Body Labor",
"hours": 7.4,
"rate": 58,
"cost": 429.2
},
{
"label": "Paint Labor",
"hours": 4.8,
"rate": 58,
"cost": 278.4
},
{
"label": "Paint Supplies",
"hours": 4.8,
"rate": 42,
"cost": 201.6
}
],
"subtotal": 2751.7,
"salesTax": 227.02,
"taxRate": 8.25,
"grandTotal": 2978.72,
"netTotal": 2478.72,
"deductible": 500
},
"line_count": 14,
"notes": [],
"files": [
{
"id": "b1e2f3a4-5c6d-4e7f-8a9b-0c1d2e3f4a5b",
"object": "file",
"case_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"kind": "xme",
"name": "C26006608.xme",
"bytes": 61440,
"uploaded_to_portal": false,
"download_url": "https://blnd.one/api/v1/cases/7c9e6679-7425-40de-944b-e07fc1f90ae7/files/b1e2f3a4-5c6d-4e7f-8a9b-0c1d2e3f4a5b",
"created_at": "2026-10-18T14:06:40.117Z"
},
{
"id": "c2f3a4b5-6d7e-4f80-9b1c-2d3e4f5a6b7c",
"object": "file",
"case_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"kind": "xme",
"name": "C26006608.xme",
"bytes": 41872,
"uploaded_to_portal": false,
"download_url": "https://blnd.one/api/v1/cases/7c9e6679-7425-40de-944b-e07fc1f90ae7/files/c2f3a4b5-6d7e-4f80-9b1c-2d3e4f5a6b7c",
"created_at": "2026-10-18T14:06:40.117Z"
}
],
"started_at": "2026-10-18T14:03:02.950Z",
"finished_at": "2026-10-18T14:06:40.402Z"
}
}
}Verifying signatures
Every delivery carries three headers:
| Header | Value |
|---|---|
| webhook-id | The event id, evt_…. The same on every retry of that event. |
| webhook-timestamp | Unix time, in seconds, of this attempt. |
| webhook-signature | v1, then the base64 HMAC-SHA256 of the signed content. May hold several space-separated signatures. |
To verify one:
- Take the raw request body, before any JSON parsing.
- Build the signed content:
{webhook-id}.{webhook-timestamp}.{body}. - The key is the base64-decoded part of your secret after
whsec_. - Compute HMAC-SHA256, base64-encode it, and compare it with each
v1,signature in constant time. - Reject timestamps more than 5 minutes from now, so an old delivery can't be replayed.
Standard Webhooks libraries verify these headers as they are. The test vector beside this was signed by blnd's own signer: your check should accept it (ignore the timestamp tolerance for that test).
import crypto from "node:crypto";
import express from "express";
const secret = process.env.BLND_WEBHOOK_SECRET; // whsec_…
const TOLERANCE_S = 5 * 60;
export function verifyWebhook(rawBody, headers) {
const id = headers["webhook-id"];
const timestamp = headers["webhook-timestamp"];
const signatures = headers["webhook-signature"];
if (!id || !timestamp || !signatures) throw new Error("Missing webhook headers");
// Reject old (or future) timestamps: replay protection
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > TOLERANCE_S) {
throw new Error("Timestamp outside the tolerance");
}
const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
const expected = crypto
.createHmac("sha256", key)
.update(`${id}.${timestamp}.${rawBody}`)
.digest();
// The header may carry several space-separated signatures: "v1,<base64> v1,<base64>"
const ok = signatures.split(" ").some((part) => {
const [version, sig] = part.split(",");
const got = Buffer.from(sig ?? "", "base64");
return version === "v1" && got.length === expected.length && crypto.timingSafeEqual(got, expected);
});
if (!ok) throw new Error("Invalid signature");
return JSON.parse(rawBody);
}
const app = express();
// Verify against the raw bytes: parse the JSON only after the check
app.post("/webhooks/blnd", express.raw({ type: "application/json" }), (req, res) => {
let event;
try {
event = verifyWebhook(req.body.toString("utf8"), req.headers);
} catch {
return res.sendStatus(400);
}
res.sendStatus(204); // answer fast, then do the work
queue.add(event.type, event); // dedupe on event.id (= webhook-id)
});secret = whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw
webhook-id = evt_8c1f2e7d4a9b3c6e0f5a1d2b
webhook-timestamp = 1792332400
body = {"id":"evt_8c1f2e7d4a9b3c6e0f5a1d2b","object":"event","type":"case.created","livemode":false,"created_at":"2026-10-18T14:06:40.000Z","data":{"object":{"id":"7c9e6679-7425-40de-944b-e07fc1f90ae7","object":"case"}}}
webhook-signature = v1,f20wpH3s+o397OLg8QQfHSnD3so2AdU0s4cIZqgOU4c=Retries
A 2xx answer within 15 seconds is a success. Anything else (another status, a timeout, a refused connection) is retried on this schedule, each attempt with the same webhook-id:
| Attempt | Waits | Since the event |
|---|---|---|
| 1 | None: sent at once | 0 |
| 2 | 5 s | ≈ 5 s |
| 3 | 5 min | ≈ 5 min |
| 4 | 30 min | ≈ 35 min |
| 5 | 2 h | ≈ 3 h |
| 6 | 5 h | ≈ 8 h |
| 7 | 10 h | ≈ 18 h |
| 8 | 24 h | ≈ 42 h |
Failing endpoints are switched off
Best practices
- Answer fast. Verify, store the event, return
2xx, and do the work afterwards on a queue. Slow answers time out at 15 seconds and are retried. - Dedupe on
webhook-id. Retries and resends reuse it (it equals the event'sid), so record the ids you've handled and skip repeats. - Don't rely on order. Retries can deliver an older event after a newer one. Compare
created_at, or fetch the latest state with the API. - Verify every delivery against the raw body, and keep the secret on the server.
- Use a test endpoint with test keys while you build; it only ever hears sandbox data.
Test mode
Free demo engine
Estimates on sandbox cases are written by the demo engine: real files, never billed.
Sandbox only
Test keys see and create sandbox cases only. Their estimates are never delivered into CCC.
Test webhooks
Test endpoints receive the events about sandbox data, signed exactly like live ones.
Sandbox cases carry livemode: false and show in the blnd app with a Test badge, outside the working inbox unless you turn the Test filter on. Test keys run at 30 requests a minute. Like live keys, they need an approved Developer Program workspace. When you go live, swap the key: nothing else changes.
Changelog
2026-10-01
Initial release
- Cases: list, create, retrieve, update, and each case's timeline.
- Estimates: start one from up to 64 photos, and retrieve it.
- Files: list and download the .xme and .AWF.
- Workspace and team members.
- Webhooks for 5 events, signed per Standard Webhooks.
- Test mode, idempotency keys, cursor pagination and an OpenAPI 3.1 description.
Start building today
Apply to the Developer Program in a few minutes; once approved, your first test key is one click away. Questions: support@blnd.one.