Brisk API and CRM integrations
Every time a seller finishes your Brisk link, Brisk can send the lead to your CRM within seconds. Start with Zapier if you just want leads in Podio, GoHighLevel, Follow Up Boss, HubSpot or a Google Sheet; use webhooks or the REST API if you’re wiring up your own systems.
Get your leads into your CRM#
Every time a seller finishes your Brisk link, Brisk sends the lead to your CRM within seconds: their contact details, the property, its condition, the lowest price they’d accept, the offer they saw and the result. You don’t need a developer for the first two options.
Zapier
Connect Brisk to Podio, GoHighLevel, Follow Up Boss, HubSpot, Google Sheets and 6,000+ other apps.
Best for: getting leads into the tools you already use, today.
Webhooks
Brisk posts each lead as JSON to a URL you give it, signed so you know it came from Brisk.
Best for: CRMs and tools that accept an incoming webhook URL, or your own developer.
REST API
Pull leads and update deal status from your own systems with an API key.
Best for: custom dashboards, reporting and two-way sync.
Zapier in 4 steps#
- In Brisk, open Integrations → API keys and create a key. Name it “Zapier”. Copy it now: Brisk shows a key once.
- Connect Brisk in Zapier (the invite link is on your Integrations page) and paste the key when it asks. Zapier labels the connection with your account name.
- Pick the New Lead trigger. Choose a campaign, or leave it blank for all of them, and pick a Result if you only want certain leads.
- Add your CRM as the action and map the fields. The cheat sheet below shows which Brisk field goes where.
Ready-made recipes#
These are the templates on your Integrations page. Each one is a two- or three-step Zap.
Brisk → Podio
Every new Brisk lead becomes an item in your Podio Leads app, with the offer, the seller's minimum and the condition.
- Trigger: Brisk · New Lead (pick a campaign, or leave blank for all)
- Action: Podio · Create Item in your Leads app
- Map seller name, phone, email, address, offer shown, seller minimum and gate result
Brisk → HubSpot
Create or update a HubSpot contact for each seller and open a deal with the offer amount.
- Trigger: Brisk · New Lead
- Action: HubSpot · Create or Update Contact (match on email)
- Action: HubSpot · Create Deal: amount = offer shown, stage = your first stage
Brisk → Follow Up Boss
Send each seller to Follow Up Boss as a new lead so your team's action plans start right away.
- Trigger: Brisk · New Lead (filter: Qualified, Smart offer and Seller Financing, if you only want hot leads)
- Action: Follow Up Boss · Create Person (source: Brisk, tags: Tags)
- Map phone, email, address, and put the offer and minimum in the note
Brisk → GoHighLevel
Add each seller as a GoHighLevel contact and drop them into an opportunity pipeline. Seller financing leads get a "Seller Financing" tag; on an agent's campaign, the seller's pick arrives as "Wants to list" or "Wants cash".
- Trigger: Brisk · New Lead
- Action: GoHighLevel · Create/Update Contact (tags: brisk + Tags)
- Action: GoHighLevel · Create Opportunity: value = offer shown
Brisk → Google Sheets
Log every lead as a new row in a Google Sheet: a simple shared lead list.
- Trigger: Brisk · New Lead
- Action: Google Sheets · Create Spreadsheet Row
- One column per field: date, seller, phone, address, offer shown, minimum, gate result, lead link
Brisk → your CRM, for agents
Real estate agents: every seller lands in your CRM with the estimated sale price, the cash option (when shown) and which one they picked. A later pick arrives as an update.
- Trigger: Brisk · New Lead (your campaign)
- Action: your CRM · Create or Update Contact (match on email; tags: Tags)
- Map estimated sale price, cash offer, seller choice and gate result
- Second Zap: Brisk · Lead Updated (filter: update_reason is seller_choice) → update the contact's tags
Podio deal closed → Brisk status
When a deal is marked closed in Podio, mark the lead Closed in Brisk with the contract price, so your closing numbers stay right.
- Trigger: Podio · Item Updated (filter: status is Closed)
- Search: Brisk · Find Lead (by the seller's email or phone)
- Action: Brisk · Update Lead Status: Closed, with contract price and close date
Which leads get sent#
Every finished lead fires lead.created, whatever the result: Qualified, Smart offer, Cold, Manual review, all of them. To send only some, use the Result filter on the Zapier trigger, or the results filter on a webhook (Integrations → Webhooks, or gate_result on POST /hooks). The field is called gate_result and takes these values:
| Value | Label | Meaning |
|---|---|---|
qualified | Qualified | The seller's minimum was within your tolerance; they saw your full offer. |
smart_offer | Smart offer | The seller asked for less than your offer; Smart Offer showed them a number just above their minimum. |
seller_finance | Seller Financing | Above your range but open to seller financing. No number was shown. |
high_asking_price | High asking price | Agent campaigns only: the seller's price was above the estimated sale price plus tolerance. They still saw the estimate (and the cash option, when on). |
cold | Cold | The seller's minimum was too far above your offer; they got a polite decline. |
manual_review | Manual review | Something needs a person to look first (a flag, the floor, or no data). No number was shown. |
disqualified | Disqualified | A flag action disqualified the home. The seller saw a decline. |
pending_lookup | Needs lookup (account was out of lookups) | The account was out of lookups when the seller came; contact details were saved and the lookup waits. |
in_progress | Didn't finish | The seller left contact details but didn't reach the end. |
One exception: a lead that never reached the end of the form (in_progress) doesn’t fire lead.created. Those leads only show up in GET /leads, so you can still follow up.
Mapping cheat sheet#
The fields most CRMs need, and where they usually go. Every field is explained in the field reference.
| Brisk field | Usual CRM field |
|---|---|
seller_first_name + seller_last_name | Contact name (first and last) |
seller_phone | Phone |
seller_email | |
address_line1 + city + state + zip | Property address |
offer_shown | Deal or opportunity value |
seller_minimum | Asking price |
gate_result | Lead status or pipeline stage |
tags | Tags |
lead_url | Note or link field (opens the lead in Brisk) |
Sending status back to Brisk#
Brisk’s report counts closed deals and contract prices, but only if it hears about them. The “Podio deal closed → Brisk status” recipe above does that: when you close a deal in your CRM, a Zap finds the lead in Brisk (by the seller’s email or phone) and marks it Closed with the contract price and close date. Any CRM that can trigger a Zap on a status change works the same way, and your own code can call PATCH /leads/{id}/status.
Quick start#
The base URL is https://app.briskoffers.com/api/v1. Every response is JSON.
Create a key
In Brisk, open Integrations → API keys. Keys look like brk_live_ followed by 32 letters and digits. Brisk shows a key once, stores only a fingerprint of it, and lets you revoke it at any time. A revoked key stops working immediately.
Your first call
Send the key as a bearer token. GET /me returns the account the key belongs to, so it’s the easiest way to test a connection.
curl "https://app.briskoffers.com/api/v1/me" \
-H "Authorization: Bearer brk_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"const res = await fetch("https://app.briskoffers.com/api/v1/me", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.BRISK_API_KEY}`,
},
});
const data = await res.json();{
"account_id": "00000000-0000-4000-8000-00000000000a",
"account_name": "Your company",
"handle": "your-company"
}A key gives read access to every lead in the account and can change deal status. Keep it on a server, and never put it in a browser, a mobile app or a public repository.
Authentication, rate limits and errors#
Send your key on every request:
Authorization: Bearer brk_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX- A missing header, a key that doesn’t match the
brk_live_format, an unknown key or a revoked key all return401 invalid_api_key. - The API needs an active plan or free access on the account; otherwise every call returns
403 plan_inactive. - Each key may make 120 requests per minute. Past that you get
429 rate_limitedwith aRetry-Afterheader in seconds. - Every response carries
Cache-Control: no-store.
Error format
Errors have a stable error code and a human-readable message:
{
"error": "invalid_api_key",
"message": "Send a valid API key: Authorization: Bearer brk_live_…"
}| Code | Status | When |
|---|---|---|
bad_request | 400 | The body or a parameter is missing or invalid. The message says which. |
invalid_api_key | 401 | No Authorization header, a malformed key, an unknown key, or a revoked key. |
plan_inactive | 403 | The key is valid but the account has no active plan or free access. |
not_found | 404 | The lead or subscription doesn't exist, or belongs to another account. |
rate_limited | 429 | More than the per-minute limit for this key, or too many requests with a missing or wrong key from one connection. Wait Retry-After seconds. |
server_error | 500 | Something failed on Brisk's side. Retry later. |
Endpoints#
Lead responses use the same flat shape as webhooks (see the field reference), without the raw object. Leads from the widget preview in the portal are never returned.
GET/me#
The account the key belongs to. Use it to test a connection.
curl "https://app.briskoffers.com/api/v1/me" \
-H "Authorization: Bearer brk_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"const res = await fetch("https://app.briskoffers.com/api/v1/me", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.BRISK_API_KEY}`,
},
});
const data = await res.json();{
"account_id": "00000000-0000-4000-8000-00000000000a",
"account_name": "Your company",
"handle": "your-company"
}Errors: 401, 403, 429.
GET/campaigns#
Your active (non-archived) campaigns, oldest first. Handy for a dropdown.
curl "https://app.briskoffers.com/api/v1/campaigns" \
-H "Authorization: Bearer brk_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"const res = await fetch("https://app.briskoffers.com/api/v1/campaigns", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.BRISK_API_KEY}`,
},
});
const data = await res.json();[
{
"id": "00000000-0000-4000-8000-00000000000b",
"name": "Raleigh – Direct mail",
"status": "live"
}
]Errors: 401, 403, 429.
GET/leads#
Your newest leads, or one lead found by email or phone.
| Name | Type | Required | Notes |
|---|---|---|---|
limit | integer | Optional | How many to return, newest first. Default 3, maximum 100. |
email | string | Optional | Find leads with this email (case-insensitive, exact match). |
phone | string | Optional | Find leads with this phone number, in any format: the last 10 digits are compared. Fewer than 10 digits returns 400. |
curl "https://app.briskoffers.com/api/v1/leads?limit=3" \
-H "Authorization: Bearer brk_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"const res = await fetch("https://app.briskoffers.com/api/v1/leads?limit=3", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.BRISK_API_KEY}`,
},
});
const data = await res.json();[
{
"lead_id": "00000000-0000-4000-8000-000000000000",
"seller_first_name": "TEST",
"seller_last_name": "Seller",
"seller_phone": "(555) 555-0100",
"seller_email": "test.seller@example.com",
"address_line1": "123 Test St",
"unit": null,
"city": "Raleigh",
"state": "NC",
"zip": "27610",
"county": "Wake",
"beds": 3,
"baths": 2,
"sqft": 1840,
"sqft_source": "public_records",
"year_built": 1994,
"lot_acres": 0.24,
"property_type": "Single-family",
"seller_condition": "Dated",
"seller_kitchen_condition": "Dated",
"seller_bathroom_condition": "Fixer upper",
"flags": "Roof age",
"seller_minimum": 186000,
"calculated_offer": 191400,
"offer_shown": 188000,
"smart_offer": true,
"room_kept": 3400,
"gate_result": "smart_offer",
"offer_type": "cash",
"estimated_sale_price": null,
"cash_offer": null,
"seller_choice": null,
"seller_choice_at": null,
"is_test": false,
"seller_response": null,
"counter_amount": null,
"counter_note": null,
"closing_preference": null,
"responded_at": null,
"tags": "",
"seller_finance_mortgage": null,
"condition_mismatch": false,
"county_condition_tier": "Dated",
"offer_at_county_condition": 191400,
"deal_status": "new",
"tcpa_consent_at": "2026-10-01T15:04:05.000Z",
"lead_url": "https://app.briskoffers.com/app/offers?lead=00000000-0000-4000-8000-000000000000",
"lead_created_at": "2026-10-01T15:00:00.000Z"
}
]Or find a lead:
curl "https://app.briskoffers.com/api/v1/leads?phone=5555550100" \
-H "Authorization: Bearer brk_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"const res = await fetch("https://app.briskoffers.com/api/v1/leads?phone=5555550100", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.BRISK_API_KEY}`,
},
});
const data = await res.json();Errors: 400 (phone too short), 401, 403, 429.
GET/leads/{id}#
One lead by its lead_id. An unknown id, or one that belongs to another account, returns 404.
curl "https://app.briskoffers.com/api/v1/leads/00000000-0000-4000-8000-000000000000" \
-H "Authorization: Bearer brk_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"const res = await fetch("https://app.briskoffers.com/api/v1/leads/00000000-0000-4000-8000-000000000000", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.BRISK_API_KEY}`,
},
});
const data = await res.json();{
"lead_id": "00000000-0000-4000-8000-000000000000",
"seller_first_name": "TEST",
"seller_last_name": "Seller",
"seller_phone": "(555) 555-0100",
"seller_email": "test.seller@example.com",
"address_line1": "123 Test St",
"unit": null,
"city": "Raleigh",
"state": "NC",
"zip": "27610",
"county": "Wake",
"beds": 3,
"baths": 2,
"sqft": 1840,
"sqft_source": "public_records",
"year_built": 1994,
"lot_acres": 0.24,
"property_type": "Single-family",
"seller_condition": "Dated",
"seller_kitchen_condition": "Dated",
"seller_bathroom_condition": "Fixer upper",
"flags": "Roof age",
"seller_minimum": 186000,
"calculated_offer": 191400,
"offer_shown": 188000,
"smart_offer": true,
"room_kept": 3400,
"gate_result": "smart_offer",
"offer_type": "cash",
"estimated_sale_price": null,
"cash_offer": null,
"seller_choice": null,
"seller_choice_at": null,
"is_test": false,
"seller_response": null,
"counter_amount": null,
"counter_note": null,
"closing_preference": null,
"responded_at": null,
"tags": "",
"seller_finance_mortgage": null,
"condition_mismatch": false,
"county_condition_tier": "Dated",
"offer_at_county_condition": 191400,
"deal_status": "new",
"tcpa_consent_at": "2026-10-01T15:04:05.000Z",
"lead_url": "https://app.briskoffers.com/app/offers?lead=00000000-0000-4000-8000-000000000000",
"lead_created_at": "2026-10-01T15:00:00.000Z"
}Errors: 401, 403, 404, 429.
PATCH/leads/{id}/status#
Sets the deal status, the same way your team does in the portal. The change is recorded in the lead’s history, counts toward closed-deal tracking, and fires a lead.updated webhook with update_reason: "deal_status" when the status actually changes. Returns the updated lead.
| Name | Type | Required | Notes |
|---|---|---|---|
status | string | Required | One of new, offer_accepted, counter_submitted, contacted, appointment, listing_signed, listed, under_contract, closed and dead. Labels like “Under contract” are accepted and normalized. listing_signed and listed are for real estate agent accounts only (other accounts get a 400), and agents can also send “Listing appointment” for appointment. |
contract_price | number | Optional | Dollars, positive, at most 100,000,000. Kept only on closed. |
close_date | string | Optional | YYYY-MM-DD. Kept only on closed. |
dead_reason | string | Optional | One of price, not_motivated, unreachable and other. Required on dead; ignored otherwise. |
Mark a deal closed:
curl -X PATCH "https://app.briskoffers.com/api/v1/leads/00000000-0000-4000-8000-000000000000/status" \
-H "Authorization: Bearer brk_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" \
-H "Content-Type: application/json" \
-d '{"status":"closed","contract_price":185000,"close_date":"2026-11-02"}'const res = await fetch("https://app.briskoffers.com/api/v1/leads/00000000-0000-4000-8000-000000000000/status", {
method: "PATCH",
headers: {
Authorization: `Bearer ${process.env.BRISK_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"status": "closed",
"contract_price": 185000,
"close_date": "2026-11-02"
}),
});
const data = await res.json();{
"lead_id": "00000000-0000-4000-8000-000000000000",
"seller_first_name": "TEST",
"seller_last_name": "Seller",
"seller_phone": "(555) 555-0100",
"seller_email": "test.seller@example.com",
"address_line1": "123 Test St",
"unit": null,
"city": "Raleigh",
"state": "NC",
"zip": "27610",
"county": "Wake",
"beds": 3,
"baths": 2,
"sqft": 1840,
"sqft_source": "public_records",
"year_built": 1994,
"lot_acres": 0.24,
"property_type": "Single-family",
"seller_condition": "Dated",
"seller_kitchen_condition": "Dated",
"seller_bathroom_condition": "Fixer upper",
"flags": "Roof age",
"seller_minimum": 186000,
"calculated_offer": 191400,
"offer_shown": 188000,
"smart_offer": true,
"room_kept": 3400,
"gate_result": "smart_offer",
"offer_type": "cash",
"estimated_sale_price": null,
"cash_offer": null,
"seller_choice": null,
"seller_choice_at": null,
"is_test": false,
"seller_response": null,
"counter_amount": null,
"counter_note": null,
"closing_preference": null,
"responded_at": null,
"tags": "",
"seller_finance_mortgage": null,
"condition_mismatch": false,
"county_condition_tier": "Dated",
"offer_at_county_condition": 191400,
"deal_status": "closed",
"tcpa_consent_at": "2026-10-01T15:04:05.000Z",
"lead_url": "https://app.briskoffers.com/app/offers?lead=00000000-0000-4000-8000-000000000000",
"lead_created_at": "2026-10-01T15:00:00.000Z"
}Mark a deal dead:
curl -X PATCH "https://app.briskoffers.com/api/v1/leads/00000000-0000-4000-8000-000000000000/status" \
-H "Authorization: Bearer brk_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" \
-H "Content-Type: application/json" \
-d '{"status":"dead","dead_reason":"price"}'const res = await fetch("https://app.briskoffers.com/api/v1/leads/00000000-0000-4000-8000-000000000000/status", {
method: "PATCH",
headers: {
Authorization: `Bearer ${process.env.BRISK_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"status": "dead",
"dead_reason": "price"
}),
});
const data = await res.json();{
"lead_id": "00000000-0000-4000-8000-000000000000",
"seller_first_name": "TEST",
"seller_last_name": "Seller",
"seller_phone": "(555) 555-0100",
"seller_email": "test.seller@example.com",
"address_line1": "123 Test St",
"unit": null,
"city": "Raleigh",
"state": "NC",
"zip": "27610",
"county": "Wake",
"beds": 3,
"baths": 2,
"sqft": 1840,
"sqft_source": "public_records",
"year_built": 1994,
"lot_acres": 0.24,
"property_type": "Single-family",
"seller_condition": "Dated",
"seller_kitchen_condition": "Dated",
"seller_bathroom_condition": "Fixer upper",
"flags": "Roof age",
"seller_minimum": 186000,
"calculated_offer": 191400,
"offer_shown": 188000,
"smart_offer": true,
"room_kept": 3400,
"gate_result": "smart_offer",
"offer_type": "cash",
"estimated_sale_price": null,
"cash_offer": null,
"seller_choice": null,
"seller_choice_at": null,
"is_test": false,
"seller_response": null,
"counter_amount": null,
"counter_note": null,
"closing_preference": null,
"responded_at": null,
"tags": "",
"seller_finance_mortgage": null,
"condition_mismatch": false,
"county_condition_tier": "Dated",
"offer_at_county_condition": 191400,
"deal_status": "dead",
"tcpa_consent_at": "2026-10-01T15:04:05.000Z",
"lead_url": "https://app.briskoffers.com/app/offers?lead=00000000-0000-4000-8000-000000000000",
"lead_created_at": "2026-10-01T15:00:00.000Z"
}Errors: 400 (unknown status, missing dead_reason, bad price or date), 401, 403, 404, 429.
POST/hooks#
Subscribes a URL to an event (REST hooks; this is what the Zapier app uses). Returns 201 with the subscription’s id and its signing secret, which is shown once. An account can have up to 50 subscriptions created through the API.
| Name | Type | Required | Notes |
|---|---|---|---|
url | string | Required | A public HTTPS URL. Private, loopback and cloud-metadata addresses are rejected. |
event | string | Required | lead.created and lead.updated. |
campaign_id | string (uuid) | Optional | Only leads from this campaign. Must be one of your campaigns. |
gate_result | string | Optional | Only leads with this result (see which leads get sent). Applies to lead.created. |
curl -X POST "https://app.briskoffers.com/api/v1/hooks" \
-H "Authorization: Bearer brk_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com/brisk-leads","event":"lead.created","gate_result":"qualified"}'const res = await fetch("https://app.briskoffers.com/api/v1/hooks", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.BRISK_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"url": "https://example.com/brisk-leads",
"event": "lead.created",
"gate_result": "qualified"
}),
});
const data = await res.json();{
"id": "00000000-0000-4000-8000-0000000000c1",
"event": "lead.created",
"url": "https://example.com/brisk-leads",
"signing_secret": "whsec_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
}Errors: 400 (bad URL, unknown event or campaign, too many subscriptions), 401, 403, 429.
DELETE/hooks/{id}#
Removes a subscription created through the API. Webhooks added in the portal can’t be removed this way (they return 404).
curl -X DELETE "https://app.briskoffers.com/api/v1/hooks/00000000-0000-4000-8000-0000000000c1" \
-H "Authorization: Bearer brk_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"const res = await fetch("https://app.briskoffers.com/api/v1/hooks/00000000-0000-4000-8000-0000000000c1", {
method: "DELETE",
headers: {
Authorization: `Bearer ${process.env.BRISK_API_KEY}`,
},
});
const data = await res.json();{
"id": "00000000-0000-4000-8000-0000000000c1",
"deleted": true
}Errors: 401, 403, 404, 429.
Webhooks#
Brisk posts a JSON event to your URL when a lead is created or updated. Set one up in the portal (Integrations → Webhooks: URL, events, campaigns, results) or with POST /hooks.
Events#
| Event | When |
|---|---|
lead.created | A seller finished your link, whatever the result. Filter on gate_result if you only want some. |
lead.updated | Something changed on an existing lead. update_reason says what: deal_status The deal status changed (in the portal or through the API). · condition_verified Someone on your team verified the condition after a mismatch. · resent You clicked Resend on a delivery or resumed a paused webhook. · seller_choice On a real estate agent's campaign, the seller picked "List it with me" or the cash option (seller_choice and tags change). · seller_response The seller answered their offer: they said they are interested, or sent a counteroffer (seller_response, counter_amount and deal_status change). |
The request#
A POST with a JSON body and these headers:
| Header | Value |
|---|---|
Content-Type | application/json |
Brisk-Signature | t=<unix seconds>,v1=<hex>: the HMAC-SHA256 of <t>.<raw body> with your signing secret. |
Brisk-Event-Id | The event’s id. The same on every retry and resend. |
User-Agent | Brisk-Webhooks/1.0 (+https://briskoffers.com) |
The body is an envelope around the lead: event_id, event_type, api_version, account_id, campaign_id, campaign_name, update_reason (on updates), test: true (on test sends), then every lead field, then created_at (the event’s own time; the lead’s is lead_created_at), then raw. The full sample:
{
"event_id": "00000000-0000-4000-8000-0000000000e1",
"event_type": "lead.created",
"api_version": "2026-10-01",
"account_id": "00000000-0000-4000-8000-00000000000a",
"campaign_id": "00000000-0000-4000-8000-00000000000b",
"campaign_name": "Raleigh – Direct mail",
"lead_id": "00000000-0000-4000-8000-000000000000",
"seller_first_name": "TEST",
"seller_last_name": "Seller",
"seller_phone": "(555) 555-0100",
"seller_email": "test.seller@example.com",
"address_line1": "123 Test St",
"unit": null,
"city": "Raleigh",
"state": "NC",
"zip": "27610",
"county": "Wake",
"beds": 3,
"baths": 2,
"sqft": 1840,
"sqft_source": "public_records",
"year_built": 1994,
"lot_acres": 0.24,
"property_type": "Single-family",
"seller_condition": "Dated",
"seller_kitchen_condition": "Dated",
"seller_bathroom_condition": "Fixer upper",
"flags": "Roof age",
"seller_minimum": 186000,
"calculated_offer": 191400,
"offer_shown": 188000,
"smart_offer": true,
"room_kept": 3400,
"gate_result": "smart_offer",
"offer_type": "cash",
"estimated_sale_price": null,
"cash_offer": null,
"seller_choice": null,
"seller_choice_at": null,
"is_test": false,
"seller_response": null,
"counter_amount": null,
"counter_note": null,
"closing_preference": null,
"responded_at": null,
"tags": "",
"seller_finance_mortgage": null,
"condition_mismatch": false,
"county_condition_tier": "Dated",
"offer_at_county_condition": 191400,
"deal_status": "new",
"tcpa_consent_at": "2026-10-01T15:04:05.000Z",
"lead_url": "https://app.briskoffers.com/app/offers?lead=00000000-0000-4000-8000-000000000000",
"lead_created_at": "2026-10-01T15:00:00.000Z",
"created_at": "2026-10-01T15:04:06.000Z",
"raw": {
"status": "smart",
"step": "done",
"formula_version": 1,
"offer_preset": null,
"tags": [],
"address": {
"line1": "123 Test St",
"city": "Raleigh",
"state": "NC",
"zip": "27610",
"formatted": "123 Test St, Raleigh, NC 27610"
},
"property": {
"sqft": 1840,
"beds": 3,
"baths": 2,
"yearBuilt": 1994,
"lotAcres": 0.24,
"propertyType": "Single-family",
"county": "Wake"
},
"property_source": null,
"seller_edited_fields": [],
"condition_tier": 1,
"answers": {
"occupancy": "Owner",
"kitchen": 1,
"bath": 0
},
"flags": [
"roof"
],
"arv": 315000,
"offer_low": null,
"breakdown": null,
"county_condition_score": 8,
"county_condition_tier_index": 1,
"county_assessment_year": null,
"condition_verified_at": null,
"deal": {
"status": "new",
"dead_reason": null,
"contract_price": null,
"close_date": null,
"changed_at": null
},
"consent": {
"given": true,
"text": "By checking this box, I agree that … may contact me …",
"at": "2026-10-01T15:04:05.000Z"
},
"completed_at": "2026-10-01T15:04:05.000Z"
}
}Verifying signatures#
Every delivery is signed with the endpoint’s secret (shown once when you create the webhook). Recompute the HMAC over the raw request body, before you parse the JSON, compare it in constant time, and reject timestamps more than 5 minutes old. Parsing and re-serializing the body first changes the bytes and the check fails.
import crypto from "node:crypto";
// rawBody: the request body exactly as received (a string, before JSON.parse)
export function verifyBrisk(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(",").map((kv) => kv.split("=")));
const t = Number(parts.t);
if (Math.abs(Date.now() / 1000 - t) > 300) return false; // older than 5 minutes
const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
return parts.v1?.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
}
// verifyBrisk(rawBody, req.headers["brisk-signature"], process.env.BRISK_SIGNING_SECRET)import hashlib, hmac, time
def verify_brisk(raw_body: bytes, header: str, secret: str) -> bool:
# raw_body: the request body exactly as received, before any JSON parsing
parts = dict(kv.split("=", 1) for kv in header.split(","))
t = int(parts.get("t", "0"))
if abs(time.time() - t) > 300: # older than 5 minutes
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(parts.get("v1", ""), expected)
# verify_brisk(request.get_data(), request.headers["Brisk-Signature"], os.environ["BRISK_SIGNING_SECRET"])Delivery and retries#
- Any
2xxresponse counts as delivered. Brisk waits up to 10 seconds and doesn’t follow redirects. - After a failure Brisk retries after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 12 hours: about 21 hours in total. Respond quickly and do your work afterwards.
- After 3 consecutive failed events, the account owner gets an email.
- After 3 days of continuous failure the webhook is paused and new events are held. Resume and resend missed leads on the Integrations page sends everything that was missed.
Idempotency#
Retries, the Resend button and resuming a paused webhook all reuse the same event_id, so de-duplicate on Brisk-Event-Id. Events can arrive out of order; use created_at to decide which is newest.
Testing#
Send test lead on the Integrations page posts a clearly labeled sample (the seller is “TEST Seller”) with test: true in the body. A test send gets one try and doesn’t count toward failure handling. For a first look, point a webhook at a request-inspector site such as webhook.site, then at your real endpoint.
{
"event_id": "00000000-0000-4000-8000-0000000000e1",
"event_type": "lead.created",
"api_version": "2026-10-01",
"test": true,
"seller_first_name": "TEST",
"seller_last_name": "Seller",
"…": "…"
}Lead field reference#
Every field on a lead, in webhooks and in API responses. Money is in whole dollars, times are ISO 8601 in UTC.
| Field | Type | Nullable | Description | Example |
|---|---|---|---|---|
| Seller | ||||
lead_id | string (uuid) | No | Brisk's id for the lead. Use it with GET /leads/{id} and PATCH /leads/{id}/status. | 00000000-0000-4000-8000-000000000000 |
seller_first_name | string | No | The seller's first name(s), split from the name they typed. One word goes here when there's no last name. | TEST |
seller_last_name | string | No | The seller's last name (the last word of what they typed). Empty when they gave one word. | Seller |
seller_phone | string | null | Yes | The phone number as the seller typed it. Null only on leads that never reached the contact step. | (555) 555-0100 |
seller_email | string | null | Yes | The email as the seller typed it. | test.seller@example.com |
| Address | ||||
address_line1 | string | null | Yes | Street address of the property. | 123 Test St |
unit | string | null | Yes | Unit or apartment number, when the address had one. | — |
city | string | null | Yes | City of the property. | Raleigh |
state | string | null | Yes | Two-letter state code. | NC |
zip | string | null | Yes | Five-digit ZIP code. | 27610 |
county | string | null | Yes | County, from public records. Null when the lookup found none. | Wake |
| Property | ||||
beds | number | null | Yes | Bedrooms, from public records or the seller's correction. | 3 |
baths | number | null | Yes | Bathrooms. Halves are possible (2.5). | 2 |
sqft | number | null | Yes | Living area in square feet, from public records or entered by the seller (see sqft_source). | 1840 |
sqft_source | string | null | Yes | Who the square footage came from: seller (they entered it because public records had none, or they changed it) or public_records. Null when there is no size. A seller-entered size is not verified. | public_records |
year_built | number | null | Yes | Year the home was built. | 1994 |
lot_acres | number | null | Yes | Lot size in acres. | 0.24 |
property_type | string | null | Yes | Property type as the seller confirmed it, e.g. Single-family, Townhome, Condo, Multi-family, Mobile or manufactured. | Single-family |
| Condition | ||||
seller_condition | string | null | Yes | The seller's overall condition answer, as a label (see the enumerations below). Sets the percent of ARV in your formula. | Dated |
seller_kitchen_condition | string | null | Yes | The seller's kitchen condition answer, same labels. Null on leads from before October 22, 2026. | Dated |
seller_bathroom_condition | string | null | Yes | The seller's bathroom condition answer, same labels. | Fixer upper |
flags | string | No | Issues the seller reported, as labels separated by commas (see the enumerations below). Empty when none. The list form is raw.flags. | Roof age |
condition_mismatch | boolean | No | True when the county's condition record is a lot better than what the seller said, so the offer may deserve a second look before you call. | false |
county_condition_tier | string | null | Yes | The condition the county record implies, as a label. Null when the county has no usable record. | Dated |
offer_at_county_condition | number | null | Yes | What your formula would produce at the county's condition instead of the seller's, in dollars. Null when there's no county record. | 191400 |
| Offer | ||||
offer_type | string | No | What the seller was priced for: cash (a cash offer, on investor and agency campaigns), listing (a real estate agent's estimated sale price) or listing_and_cash (the estimate with a cash option beside it). | cash |
seller_minimum | number | null | Yes | The lowest price the seller said they'd accept, in dollars. Null when they didn't reach that question. | 186000 |
calculated_offer | number | null | Yes | What your formula produced for this home, in dollars, whether or not the seller saw it. Null when no number could be calculated (needs lookup, didn't finish). | 191400 |
offer_shown | number | null | Yes | The number the seller actually saw: the Smart Offer on a Smart Offer lead, the calculated offer on a Qualified lead. Null when no number was shown (Cold, Manual review, Seller Financing, Disqualified, didn't finish). | 188000 |
smart_offer | boolean | No | True when Smart Offer showed the seller a number just above their own minimum instead of the full calculated offer. | true |
room_kept | number | null | Yes | On a Smart Offer lead: calculated_offer minus offer_shown, the margin you kept. Null otherwise. | 3400 |
estimated_sale_price | number | null | Yes | On an agent campaign, the estimated sale price the seller saw, in dollars. Null on cash-offer campaigns and when no number was shown. | null |
cash_offer | number | null | Yes | On an agent campaign with the cash option on, the cash number shown beside the estimate, in dollars. Null when no cash card was shown. | null |
| Seller response | ||||
seller_response | string | null | Yes | The seller's answer to the offer they saw: accepted ("I'm interested at this price") or countered. Null until they answer, and on leads that were shown no number. Neither is a contract. An answer fires lead.updated with update_reason seller_response, and moves a New lead to the Offer accepted or Counter submitted deal status. | null |
counter_amount | number | null | Yes | On a countered lead, the price the seller proposed, in dollars. It is always above offer_shown. Null otherwise. | null |
counter_note | string | null | Yes | On a countered lead, what the seller added under "Anything we should know?" (up to 500 characters). Null when they added nothing. | null |
closing_preference | string | null | Yes | When the seller said they'd like to close: asap, 30_days, 30_60_days or flexible. Null when they didn't say or haven't answered. | null |
responded_at | string (ISO 8601) | null | Yes | When the seller answered. Null until then. | — |
| Result and status | ||||
gate_result | string | No | What happened at the price question: the lead's result (see the enumerations below). Filter on it to send only the leads you want to your CRM. | smart_offer |
seller_choice | string | null | Yes | On an agent campaign, which card the seller picked: list ("List it with me") or cash ("Cash option"). Null until they pick, and on every other campaign. A pick fires lead.updated with update_reason seller_choice. | null |
seller_choice_at | string (ISO 8601) | null | Yes | When the seller picked. Null until then. | — |
tags | string | No | CRM tags, separated by commas: "Seller Financing" on Seller Financing leads; "Wants to list", "Wants cash" and "High asking price" on agent-campaign leads. Empty when none. The list form is raw.tags. | — |
seller_finance_mortgage | string | null | Yes | On a Seller Financing lead, the seller's answer to "Do you have a mortgage?": free_clear, mortgage or not_sure. Null on every other lead. | — |
deal_status | string | No | Where the deal stands in your pipeline (see the enumerations below). Starts at new; you or the API move it. listing_signed and listed are for real estate agent accounts only. | new |
is_test | boolean | No | True for a test lead a real estate agent made with "See what sellers see". Test leads reach webhooks, Zapier and the API only when the account turns on "Send test leads to my CRM". | false |
| Compliance | ||||
tcpa_consent_at | string (ISO 8601) | null | Yes | When the seller checked the consent box on the form. Null when they didn't consent to calls and texts. | 2026-10-01T15:04:05.000Z |
| Links and timestamps | ||||
lead_url | string (URL) | No | A link that opens this lead in the Brisk portal. Good for a note or link field in your CRM. | https://app.briskoffers.com/app/offers?lead=00000000-0000-4000-8000-000000000000 |
lead_created_at | string (ISO 8601) | null | Yes | When the seller submitted the form. On a webhook, the envelope's created_at is the event's own time, which is later for updates. | 2026-10-01T15:00:00.000Z |
Enumerations#
gate_result
| Value | Label |
|---|---|
qualified | Qualified |
smart_offer | Smart offer |
seller_finance | Seller Financing |
high_asking_price | High asking price |
cold | Cold |
manual_review | Manual review |
disqualified | Disqualified |
pending_lookup | Needs lookup (account was out of lookups) |
in_progress | Didn't finish |
deal_status
| Value | Label |
|---|---|
new | New |
offer_accepted | Offer accepted |
counter_submitted | Counter submitted |
contacted | Contacted |
appointment | Appointment (Listing appointment for agent accounts) |
listing_signed | Listing signed (agent accounts only) |
listed | Listed (agent accounts only) |
under_contract | Under contract |
closed | Closed |
dead | Dead |
dead_reason
| Value | Label |
|---|---|
price | Price |
not_motivated | Not motivated |
unreachable | Unreachable |
other | Other |
seller_condition and the room conditions
| Label | What the seller picked |
|---|---|
Fixer upper | Fixer-upper |
Dated | Original & dated |
Standard | Well maintained |
High end | Fully renovated |
Luxury | High-end custom |
flags
| Label | Short |
|---|---|
Roof age | Old roof |
HVAC age | Old HVAC |
Solar panels | Solar panels |
Fire damage | Fire damage |
Asbestos | Asbestos |
Mobile or manufactured | Mobile / manufactured |
Well water | Well water |
Septic system | Septic |
seller_finance_mortgage
| Value | Meaning |
|---|---|
free_clear | Free and clear |
mortgage | Has mortgage |
not_sure | Not sure |
Fields people misread#
calculated_offeris what your formula produced;offer_shownis what the seller actually saw. They match on a Qualified lead. On a Smart Offer leadoffer_shownis lower, and on Cold, Manual review, Seller Financing and Disqualified leads it is null because no number was shown.smart_offeris true when Smart Offer showed a number just above the seller’s own minimum;room_keptis the difference you kept (calculated_offerminusoffer_shown).condition_mismatchflags a lead where the county’s record says the home is in better shape than the seller said.county_condition_tieris the county’s view andoffer_at_county_conditionis what your formula would produce at that condition, so you can compare before you call.tagsis a comma-separated string, ready for a tags field. The list form israw.tags.tcpa_consent_atis null when the seller didn’t check the consent box. Respect it before you call or text.
The raw object#
Webhook bodies also carry raw: the full detail behind the flat fields. It is for advanced use and may gain fields. It never contains session tokens, IP hashes or IP addresses.
| Key | What it holds |
|---|---|
status | The lead's status as stored (smart, qualified, seller_finance, high_ask, cold, review, disqualified, pending_lookup, in_progress). |
step | The last step the seller completed on the form. |
formula_version | The version of your campaign's formula that priced this lead. |
offer_preset | The offer strategy that version was published with: conservative, balanced, aggressive or custom. Null on leads from before presets. |
tags | The CRM tags as a list (the flat tags field joins them with commas). |
seller_finance | Only on Seller Financing leads: { mortgage } with the seller's mortgage answer. |
cash | Only on agent-campaign leads: { offer, formula_version } for the cash option, or null when no cash card was shown. The version names the Brisk investor formula that priced it. |
seller_choice | Only on agent-campaign leads: { choice, at } once the seller picked a card, or null. |
address | The address as structured fields plus the formatted string. |
property | Property details from public records and the seller's corrections. |
property_source | Where the property details came from. |
seller_edited_fields | Which property fields the seller corrected on the form. |
condition_tier | The seller's overall condition as an index (0 = Fixer upper … 4 = Luxury). |
answers | Every answer the seller gave on the form, keyed by question. |
flags | Reported issues as a list of keys (roof, hvac, solar, fire, asbestos, mobile, well, septic). |
arv | The after-repair value the formula used, in dollars. |
offer_low | The low end of the range, on campaigns that show a range. |
breakdown | The line-by-line calculation behind calculated_offer. |
county_condition_score | The county's condition score, when the county publishes one. |
county_condition_tier_index | The condition tier the county record implies, as an index. |
county_assessment_year | The year of the county record. |
condition_verified_at | When someone on your team verified the condition after a mismatch. |
deal | Deal status detail: status, dead_reason, contract_price, close_date, changed_at. |
consent | The consent record: given, the exact text shown, and when. |
completed_at | When the seller reached the end of the form. |
Versioning and changes#
Every webhook body carries api_version, currently 2026-10-01. Additive changes ship without a version change: new fields, new values in an enumeration, new update_reasons. Write your receiver to ignore fields it doesn’t know and to handle values it hasn’t seen. Breaking changes get a new api_version and notice by email before they take effect.
Changelog#
- 2026-10-01Initial release: lead.created and lead.updated webhooks, the REST API (/me, /campaigns, /leads, /leads/{id}, /leads/{id}/status and /hooks) and the Zapier app.
Help#
Questions? Use Get help in the Brisk app or email hello@briskoffers.com. Include the Brisk-Event-Id or lead_id when you ask about a specific delivery.
Looking for Brisk itself? Instant offers for real estate investors. Methods used on this page: GETPOSTPATCHDELETE