Partner API Integration Guide
Send us your business listings, we publish them on the BizScout marketplace, and buyer enquiries come back to you. Everything an engineering team needs to build that, without a call.
- Version
- 2.0
- Current as of
- 4 September 2026
- API version
v1- Audience
- Integrating engineers
{partner_slug} is the identifier we issue you at onboarding. Examples throughout use a fictional partner, acme-deals — substitute your own slug and never ship the example value.
01What this integration does
You send us business listings. We publish them on the BizScout marketplace. When a buyer enquires, we send you their details so your team can follow up. When a listing's marketplace status changes, we tell you.
| Direction | What travels | How |
|---|---|---|
| You → BizScout | Listings: create, update, withdraw. Optionally status and deal events. | You call our REST API |
| BizScout → You | Buyer enquiries; listing status changes | We POST to a receiver you host |
The happy path
POST /listings. We answer 201 withpublication_state: "queued".- We publish it automatically, normally within seconds.
- We POST
listing.status_changedwithpublication_state: "published". AGETnow returns apublic_url. - A buyer enquires. We POST
buyer_inquiry.createdwith their contact details. - When the business sells, tell us —
PATCHtoSOLD, or send a deal event — and we take it off the marketplace.
If your receiver is unavailable at step 3 or 4, we retry for about 24 hours, and you can catch up by reading the API (section 8).
02Getting started
| We issue you | Purpose |
|---|---|
| Partner slug | Your identifier, and a path segment in every URL. |
| API key | Sent as X-API-Key on every request. |
| Signing secret | Used both to sign your requests to us and to verify the signature on the webhooks we send you. |
| Base URL per environment | Issued at onboarding. |
| You give us | Notes |
|---|---|
| Webhook base URL | A base, not a full endpoint — we append /inquiry and /status-change. No query string, and it must not already end in one of those paths. |
| Receiver credential (optional) | If your receiver requires a credential on incoming requests — a subscription key, an API key, a bearer token — give us the value and the header it expects. We send it on every delivery, and we will match whatever your system needs. |
| Expected volumes | So we can size limits with you before go-live. |
URL shape
Versioned and slug-scoped. There is no /api segment.
https://partner.api.bizscout.com/v1/{partner_slug}/listings
# for acme-deals:
https://partner.api.bizscout.com/v1/acme-deals/listingsHow your requests reach us
Your requests arrive at our API gateway. It validates your X-API-Key and forwards the request to the API, which verifies your signature and does the work.
That means a rejection can come from either layer, and the two look different:
| Refused at the gateway | Refused by the API | |
|---|---|---|
| Status | Always 403 | 400, 403 or 404 |
| Body | Exactly {"message":"Forbidden"} | {"error":{"code","message","request_id"}} |
Has a request_id? | No | Yes |
| Cause | API key missing, wrong, or not issued for the environment you are calling — and also an unknown path or an unrecognised partner slug. All of these return an identical response, so the body cannot tell you which one it was. | Signature, validation, unknown listing, suspended account — and code tells you which. |
Use the body shape to decide what to debug. A 403 with no request_id never reached the API — check the key, the slug and the base URL rather than your signing code. A signature failure always comes back in the envelope, with a request_id.
Two other responses also arrive without the envelope: a 429 if you are throttled at the edge, and a 413 if the body exceeds 10 MB — that one is rejected before the request reaches our error handling.
IP allowlisting. We do not require you to call us from fixed addresses. If you need us to allowlist your egress IPs, tell us at onboarding — and tell us just as clearly if you cannot guarantee fixed addresses, so we never build an edge rule that silently blocks you.
03Authentication
Every request carries your API key. Every request that changes something also carries an HMAC signature.
Sign every request that changes something — POST, PATCH and DELETE. Only the read-only methods GET, HEAD and OPTIONS are exempt, because they change nothing. A DELETE carries no body but is not exempt.
If signing does not fit your system, tell us and we can turn signature verification off for your integration. It is configured per partner, so it is not a precondition for going live. We would rather you signed — the signature is what proves a request came from you and not from someone who obtained your API key — but the choice is yours.
| Header | Value | Required on |
|---|---|---|
X-API-Key | The key we issued | Every request |
X-BizScout-Signature | See below | Every request except GET, HEAD, OPTIONS |
Content-Type | application/json | Every request with a body |
A request that fails authentication returns 403 with a deliberately uninformative message.
Content-Type must be application/json, and this affects your signature. If your HTTP client defaults to form encoding, we cannot read the raw bytes we need to verify against, and the request is refused. This catches people whose client silently chooses a different encoding.
3.1Signing your requests
X-BizScout-Signature: t=<unix_seconds>,v1=<hex_digest>
v1 = HMAC_SHA256(signing_secret, "<t>" + "." + <raw request body bytes>)Three rules that cause most first-integration failures:
- Sign the exact bytes you transmit. Never re-serialise parsed JSON to build the signing message — key order and whitespace will differ and every signature will fail.
- A request with no body signs the empty string — not
{}, notnull. This applies toDELETE, which is not exempt. - The timestamp window is 300 seconds either side. A clock running fast fails exactly like one running slow. Intermittent 403s on otherwise valid requests are almost always NTP.
Send only one signature header. If something in your network path duplicates or joins it, we treat it as ambiguous and refuse the request.
const { createHmac } = require('node:crypto');
function sign(secret, rawBody) {
const t = Math.floor(Date.now() / 1000);
const v1 = createHmac('sha256', secret)
.update(`${t}.`)
.update(rawBody) // '' for a bodyless request
.digest('hex');
return `t=${t},v1=${v1}`;
}
const body = JSON.stringify(payload); // sign and send the SAME string
await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': API_KEY,
'X-BizScout-Signature': sign(SIGNING_SECRET, body),
},
body,
});3.2Verifying our webhooks
Same scheme, inverted. Verify over the raw body as received, before parsing.
const { createHmac, timingSafeEqual } = require('node:crypto');
function verify(secret, header, rawBody) {
const parts = Object.fromEntries(
String(header).split(',').map(p => p.split('=').map(s => s.trim()))
);
const t = Number(parts.t);
if (!Number.isFinite(t)) return false;
// Reject anything outside 300 seconds, in either direction.
if (Math.abs(Math.floor(Date.now() / 1000) - t) > 300) return false;
const expected = createHmac('sha256', secret)
.update(`${parts.t}.`) // the timestamp EXACTLY as sent
.update(rawBody)
.digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(String(parts.v1 || ''));
return a.length === b.length && timingSafeEqual(a, b);
}Use a constant-time comparison, and make sure your framework gives you the raw body — many parse and discard it by default.
04Conventions
4.1Status codes
| Code | Meaning |
|---|---|
200 | Success. Also a replayed POST /listings for a listing we already hold. |
201 | Listing created. |
202 | Accepted for asynchronous processing (batch, events). |
204 | Listing withdrawn. |
400 | Validation failure, or an unrecognised field. |
403 | Authentication failure, or a suspended account. |
404 | Unknown partner_listing_id or batch id. |
413 | Body over 10 MB. |
500 / 503 | Our side. Retry with the same request; it is safe to. |
4.2Errors
{
"error": {
"code": "bad_request",
"message": "Validation failed",
"request_id": "1e0e9c01-f0eb-4dff-9d62-c73be1762a44",
"details": [
{ "message": "title must be shorter than or equal to 200 characters" },
{ "message": "property listing_titel should not exist" }
]
}
}| Field | Notes |
|---|---|
code | One of bad_request, forbidden, not_found, internal_error. |
message | Human-readable. For validation failures this is always the literal "Validation failed" — the detail is in details. |
request_id | Quote it when you contact us. Present only in error bodies — never as a header, never on success, and not on per-item errors inside a batch report. |
details | Present only for field-level validation failures. Omitted otherwise. |
4.3Idempotency
POST /listingsis keyed onpartner_listing_id. A repeat returns 200 with the listing as it already exists — the replayed body is discarded. This is not an upsert; usePATCHto change a listing.- Batches are safe to resubmit. Items already created report
existing. - Events are keyed on
event_id, permanently. A duplicate returns 202 with the original acknowledgement and is not applied twice. Never generate a newevent_idfor a retry.
4.4Sending null
Optional fields accept an explicit null, and for most of them null clears the stored value.
To leave a field unchanged in a PATCH, omit the key. A client that serialises its whole model on every update — including nulls for fields it does not track — will quietly blank out data.
Two exceptions: url: null becomes an empty string, and location: null is ignored.
4.5Limits, money and time
- Bodies are capped at 10 MB.
- Money is whole dollars.
1250000means $1,250,000. No decimals, no currency field — all values are USD. - Time is ISO 8601 in UTC.
- Unknown fields are rejected with a 400, so a typo fails loudly instead of being silently dropped. Put your own data in
metadata, which we store and echo back untouched.
05Endpoints
All paths are relative to https://<base>/v1/{partner_slug}. Each endpoint below carries its own request fields, responses and failure modes.
| Method | Path | Purpose | Success |
|---|---|---|---|
POST | /listings | Create a listing | 201, or 200 on replay |
POST | /listings/batch | Create many, asynchronously | 202 |
GET | /listings/batch/{batch_id} | Poll a batch | 200 |
GET | /listings/{partner_listing_id} | Read current state | 200 |
PATCH | /listings/{partner_listing_id} | Partial update | 200 |
DELETE | /listings/{partner_listing_id} | Withdraw (reversible) | 204 |
GET | /listings/{partner_listing_id}/inquiries | Replay buyer enquiries | 200 |
POST | /listing-events | Push a status change (optional) | 202 |
POST | /deal-events | Push a deal stage change (optional) | 202 |
GET | /status | Connectivity check | 200 |
Create one listing. This endpoint defines the listing shape — PATCH and each item of a batch accept the same fields.
Required
| Field | Type | Notes |
|---|---|---|
partner_listing_id | string, max 128 | Your identifier, and the listing's identity for the whole integration. Every other call references it. Must be stable forever. |
title | string, max 200 | Non-empty. |
description | string | Non-empty. No maximum. |
industry | string | Free text, non-empty. Not validated against a list. |
listing_status | enum | ACTIVE · UNDER_LOI · SOLD · WITHDRAWN. Case-sensitive. See section 6 for what each one does. |
Five fields. Everything below is optional.
Optional — identity and headline
| Field | Type | Notes |
|---|---|---|
url | string, max 2048 | Your listing's page. Not required, not unique — two listings may share one. |
website | string, max 2048 | The business's own site. |
headline | string | Short summary line. |
metadata | object | Free-form. Stored and echoed back unchanged — use it for your own identifiers. |
Optional — financials
All whole dollars, no decimals, no currency field.
| Field | Type | Notes |
|---|---|---|
asking_price | integer | What the business is listed at. |
cash_flow | integer | Seller's discretionary earnings. |
gross_revenue | integer | Annual revenue. |
ebitda | integer | — |
inventory | integer | Value of stock included in the sale. |
rent | integer | Monthly rent. |
Optional — location
| Field | Type | Notes |
|---|---|---|
detailed_location | string | Human-readable, e.g. "Phoenix, AZ". We geocode it. |
location | object | { "lat": number, "lng": number } — both required if present, lat −90 to 90, lng −180 to 180. Supplying it skips our geocoding. |
Optional — broker contact
| Field | Type | Notes |
|---|---|---|
broker_name | string | — |
broker_phone | string | No format validation. |
broker_email | string | Must be a valid email address. |
Listing content, including the broker name, phone and email you supply, appears on a public marketplace page. Send only contact details you are entitled to publish.
Optional — media and detail
| Field | Type | Notes |
|---|---|---|
images | array of string | Max 25. Every entry must be a well-formed URL — stricter than url itself. |
established | string | Free text, not a date. "2008" is typical. |
detailed_employees | integer | Headcount. |
detailed_building_sf | string | A string, not a number. |
detailed_real_estate | string | Free text. |
detailed_lease_expiration | string | Free text, not a date. |
detailed_facilities | string | Free text. |
detailed_competition | string | Free text. |
detailed_growth_expansion | string | Free text. |
detailed_support_training | string | Free text. |
detailed_reason_for_selling | string | Free text. |
Request
POST https://partner.api.bizscout.com/v1/acme-deals/listings
X-API-Key: <key>
X-BizScout-Signature: t=1788530199,v1=<hex>
Content-Type: application/json
{
"partner_listing_id": "f0512332-435d-4b56-a546-859d18de38ba",
"title": "Established Residential HVAC Service Company",
"description": "Full-service residential HVAC company with 18 years of operating history.",
"industry": "HVAC Services",
"listing_status": "ACTIVE",
"asking_price": 2450000,
"cash_flow": 610000,
"detailed_location": "Phoenix, AZ",
"broker_name": "Dana Whitfield",
"broker_email": "dana@acme-deals.example.com",
"metadata": { "internal_ref": "AD-2026-114" }
}Response — 201 Created
{
"partner_listing_id": "f0512332-435d-4b56-a546-859d18de38ba",
"bizscout_listing_id": "4067763",
"listing_status": "ACTIVE",
"publication_state": "queued",
"title": "Established Residential HVAC Service Company",
"asking_price": 2450000,
"metadata": { "internal_ref": "AD-2026-114" },
"created_at": "2026-09-04T10:14:22.881Z",
"updated_at": "2026-09-04T10:14:22.881Z"
}| Response field | Notes |
|---|---|
bizscout_listing_id | Our id, as a string. Store it — every webhook carries it. |
publication_state | queued · published · unpublished. Ours, not yours — see section 6. |
public_url | Present only while publication_state is published, so never on a create response. |
| Everything you sent | Echoed back, and omitted entirely if never set — expect absent keys rather than nulls. |
- Also
- 200 if we already hold that
partner_listing_id— the existing listing is returned and your body is discarded. - Errors
- 400 validation or unknown field · 403 authentication.
A listing created with a status other than ACTIVE is stored without being published. If a business is already under offer or sold when you first sync it, send its real status and we keep it off the marketplace until you move it to ACTIVE.
Create many listings asynchronously. Each item takes exactly the fields in 5.1.
POST .../listings/batch
{ "listings": [ { …create fields… }, { …create fields… } ] }Response — 202 Accepted
{
"batch_id": "58a58536-d849-4f5b-ac7e-f5a579559f3a",
"status": "queued",
"total": 2,
"status_url": "https://partner.api.bizscout.com/v1/acme-deals/listings/batch/58a58536-…"
}- Limits
- Up to 100 listings per request; confirm your environment's limit with us at onboarding. An empty array is accepted as a no-op.
- Errors
- 400 over the cap, a duplicate
partner_listing_idwithin the same envelope, or any invalid item · 403.
Validation is all-or-nothing — one bad item rejects the whole request with a 400 naming its index, and nothing is created. Once accepted, items are processed independently and one failure never stops the rest.
Poll a batch.
Response — 200 OK
{
"batch_id": "58a58536-d849-4f5b-ac7e-f5a579559f3a",
"status": "completed",
"total": 2,
"processed": 2,
"summary": { "total": 2, "created": 1, "existing": 1, "failed": 0 },
"results": [
{ "partner_listing_id": "…", "status": "created", "listing": { "bizscout_listing_id": "4067764" } },
{ "partner_listing_id": "…", "status": "existing", "listing": { "bizscout_listing_id": "4067701" } }
]
}| Field | Values |
|---|---|
status | queued · processing · completed · failed |
summary, results | Present once completed. |
results[].status | created · existing (an idempotent replay) · failed, which carries an error with code and message but no request_id. |
- Errors
- 404 unknown id, a batch belonging to another partner, or an expired report.
Reports expire after about 24 hours, after which the id returns 404. The listings are permanent; only the report goes — capture it if you need a durable record. A failed batch is safe to resubmit: already-created items report existing.
Read current state. This is the authoritative answer for listing_status and publication_state.
- Response
- 200 with the listing object as shown in 5.1, plus
public_urlwhile published. - Errors
- 404 unknown
partner_listing_id.
A withdrawn or sold listing still returns 200. Withdrawal deletes nothing, so you cannot use a 404 to infer a listing is gone — read listing_status instead.
Partial update. Accepts any subset of the fields in 5.1 — send only what changed.
PATCH .../listings/f0512332-435d-4b56-a546-859d18de38ba
{ "asking_price": 2295000 }- Response
- 200 with the updated listing.
- Errors
- 400 validation, unknown field, or
partner_listing_idin the body — it comes from the path · 403 · 404.
Omit a key to leave it unchanged; see 4.4 before sending null. Changing listing_status applies the lifecycle rules in section 6.
Withdraw a listing. No request body.
DELETE .../listings/f0512332-435d-4b56-a546-859d18de38ba
X-API-Key: <key>
X-BizScout-Signature: t=1788530412,v1=<hex over the EMPTY string>- Response
- 204, no body.
listing_statusbecomesWITHDRAWNand the listing leaves the marketplace. - Errors
- 403 · 404.
A soft delete — nothing is destroyed, a later PATCH to ACTIVE republishes it, and withdrawing twice is not an error. Remember this request is still signed, over the empty string.
Replay the buyer enquiries we have sent, or attempted to send, for this listing. Your catch-up path if your receiver was unavailable.
| Query | Type | Notes |
|---|---|---|
limit | integer | 1–100. Default 25. |
cursor | string | Opaque. Pass back the previous page's next_cursor. |
created_after | string | Must be a valid ISO-8601 timestamp. |
{ "data": [ { …enquiry… } ], "has_more": true, "next_cursor": "18240" }- Response
- Each element of
datais thedatablock of abuyer_inquiry.createdevent, identical to what the webhook carried — see 7.3. - Errors
- 400
limitout of range · 404 unknown listing.
Push a status change as an event instead of a PATCH. Most integrations use PATCH.
| Field | Type | Required | Notes |
|---|---|---|---|
event_id | string, max 128 | Yes | Your id for this event. The deduplication key — never regenerate it for a retry. |
event_type | literal | Yes | Only listing.status_changed is accepted. |
partner_listing_id | string | Yes | The listing this is about. |
status | enum | Yes | ACTIVE · UNDER_LOI · SOLD · WITHDRAWN. |
occurred_at | string | Yes | ISO-8601. Recorded for audit — arrival order decides, not this value. |
previous_status | enum | No | Recorded, not used for resolution. |
reason | string | No | Free text. |
metadata | object | No | Free-form. Recorded with the event; not echoed back in the acknowledgement. |
POST .../listing-events
{
"event_id": "0d1f4c22-9d0e-4a57-9f7b-2f1f1f0f9a10",
"event_type": "listing.status_changed",
"partner_listing_id": "f0512332-435d-4b56-a546-859d18de38ba",
"status": "UNDER_LOI",
"occurred_at": "2026-09-04T10:00:00Z"
}
→ 202 { "accepted": true, "event_id": "0d1f4c22-…", "bizscout_event_id": "18240" }- Response
- 202. A duplicate
event_idreturns the original acknowledgement and is not applied twice. - Errors
- 400 validation · 404 unknown
partner_listing_id.
Create the listing first, and confirm it succeeded. An event for a listing we do not hold returns 404 and is not recorded — retrying with the same event_id gets the same 404 indefinitely.
Push a deal stage change.
| Field | Type | Required | Notes |
|---|---|---|---|
event_id | string, max 128 | Yes | Deduplication key. |
event_type | literal | Yes | Only deal.stage_changed is accepted. |
partner_listing_id | string | Yes | The listing the deal is on. |
partner_deal_id | string | Yes | Your id for the deal. Multiple deals may sit on one listing. |
stage | string | Yes | Open enum — any non-empty string is accepted and recorded, so your own vocabulary is safe to send. One value has side effects; see below. |
occurred_at | string | Yes | ISO-8601. Arrival order decides. |
previous_stage | string | No | Free text. |
amount | integer | No | Whole dollars. |
bizscout_inquiry_id | string | No | The inquiry_id we sent you, to join the deal back to the buyer enquiry. |
broker | object | No | { name, email, phone, firm } — all optional; email must be valid if present. |
buyer | object | No | Same shape as broker. |
metadata | object | No | Free-form. Recorded with the event; not echoed back in the acknowledgement. |
POST .../deal-events
{
"event_id": "9a41c0de-7f22-4a10-8b6e-0d51b2c8e441",
"event_type": "deal.stage_changed",
"partner_listing_id": "f0512332-435d-4b56-a546-859d18de38ba",
"partner_deal_id": "ad_deal_301",
"stage": "DEAL_CLOSED_WON",
"occurred_at": "2026-09-04T10:30:00Z"
}- Response
- 202, same acknowledgement and duplicate handling as 5.8.
- Errors
- 400 validation · 404 unknown
partner_listing_id.
DEAL_CLOSED_WON sets the listing to SOLD and unpublishes it, moves every other open deal on that listing to SOLD_TO_ANOTHER_BUYER, and sends one listing.status_changed webhook.
Reversing it does not reopen those sibling deals — weeks may have passed and only you know whether those buyers are still live, so reopen them explicitly.
5.10Event ordering
Send one event at a time per listing, and one at a time per deal, waiting for the 202 before the next. Events apply in the order they arrive, not by occurred_at, so firing them concurrently can leave a listing or deal in the wrong final state — with no error to tell you.
Concurrency across different listings and deals is fine.
Connectivity check through the full chain.
- Response
- 200 with
{"status":"ok","partner":"acme-deals"}.
This confirms your key and routing only — GET is not signed, so it tells you nothing about your signing.
06The listing lifecycle
Two values move independently. You own one; we own the other.
| Value | Owner | Meaning |
|---|---|---|
listing_status | You | Where the business is in its sale: ACTIVE, UNDER_LOI, SOLD, WITHDRAWN. |
publication_state | BizScout | Whether we are showing it: queued, published, unpublished. |
You set listing_status to | Result |
|---|---|
ACTIVE, from a non-active state | queued, then published — back on the marketplace |
UNDER_LOI, SOLD or WITHDRAWN | unpublished — leaves the marketplace |
UNDER_LOI unpublishes. A business under a letter of intent comes off the marketplace, not merely flagged. If you want it visible while under offer, keep it ACTIVE and track the LOI on your side.
Re-sending a status a listing already has changes nothing and produces no webhook. Neither does a change between two unpublished states, such as UNDER_LOI to SOLD.
What “published” means: publication_state is published, the listing is live and searchable on the marketplace, and the API returns a public_url. Getting there normally takes seconds — we enrich the listing first, and there is no human review step.
If a listing stays queued for more than a few minutes, contact us with the request_id from the create. Re-sending the create will not move it — it is an idempotent replay and returns the listing unchanged.
07Webhooks you must receive
Give us one base URL. We POST beneath it:
| Path | Event |
|---|---|
<your_base>/inquiry | buyer_inquiry.created |
<your_base>/status-change | listing.status_changed |
Route on the path, or on the event field in the body — both are reliable.
7.1Headers on every delivery
| Header | Value |
|---|---|
Content-Type | application/json |
X-BizScout-Event | The event type. |
X-BizScout-Delivery | <envelope_id>:<attempt_number> — how you tell a retry from a first attempt. |
X-BizScout-Signature | Verify it as in 3.2. |
| Your credential header | If you gave us a credential at onboarding, we send it on every delivery in the header your system expects. |
7.2Envelope
| Field | Type | Notes |
|---|---|---|
id | string | evt_<uuid>. Your deduplication key — stable across every retry. |
event | enum | listing.status_changed or buyer_inquiry.created. |
api_version | string | v1. |
occurred_at | string | ISO-8601. Stamped once and stable across retries. |
data | object | Per-event. See below — the two events differ. |
7.3Payloads
listing.status_changed puts the listing ids directly on data. buyer_inquiry.created puts them under data.listing. Handle each explicitly.
listing.status_changed
{
"id": "evt_77c94ff2-1a90-42f1-b595-5b7c3d22c1c5",
"event": "listing.status_changed",
"api_version": "v1",
"occurred_at": "2026-09-03T23:07:10.866Z",
"data": {
"bizscout_listing_id": "1532472",
"partner_listing_id": "2320db81-0b25-4975-9ece-eac35b2575b5",
"publication_state": "published",
"listing_status": "ACTIVE",
"changed_at": "2026-09-03T23:07:10.866Z"
}
}data field | Notes |
|---|---|
bizscout_listing_id | Our id, as a string. |
partner_listing_id | Yours. Use it to find your record. |
publication_state | The new state. |
listing_status | The new status. Carried alongside the publication state, so one event tells you the whole picture — including “left the marketplace because it sold”. |
changed_at | ISO-8601. |
buyer_inquiry.created
{
"id": "evt_6c342746-2561-4643-879d-24f52ba3f689",
"event": "buyer_inquiry.created",
"api_version": "v1",
"occurred_at": "2026-08-17T22:22:59Z",
"data": {
"inquiry_id": "bs_inq_10482",
"buyer": {
"first_name": "Michael",
"last_name": "Anderson",
"email": "michael.anderson@example.com",
"phone": "+1-415-555-0187",
"role": "pro"
},
"listing": {
"bizscout_listing_id": "4067763",
"partner_listing_id": "f0512332-435d-4b56-a546-859d18de38ba"
},
"purchase_timeframe": "0-3 months",
"message": "Hi, I am interested in acquiring this business...",
"created_at": "2026-08-17T22:22:59Z"
}
}data field | Notes |
|---|---|
inquiry_id | Our id for the enquiry. Send it back on a deal event to join the deal to the lead. |
buyer.first_name, last_name, email, phone | Always present. |
buyer.role | The buyer's BizScout subscription tier, free or pro. The key is omitted entirely when we cannot determine it — absent means unknown, not free. |
listing | Only the two identifiers. No listing name, location or broker details — look those up from your own record using partner_listing_id. |
purchase_timeframe | What the buyer told us, e.g. "0-3 months". |
message | The buyer's message. An empty string when they left none — never null. |
created_at | ISO-8601. |
7.4Retries
Answer 2xx as soon as you have durably accepted the event, then process. Each request times out after 10 seconds.
| You answer | We |
|---|---|
Any 2xx | Mark it delivered. |
403 | Retry — we assume transient auth trouble and re-sign each attempt. |
Any other 4xx | Stop. The same bytes cannot become acceptable. |
5xx, timeout, connection error | Retry. |
| Attempt | When |
|---|---|
| 1 | immediately |
| 2 | +5 minutes |
| 3 | +35 minutes |
| 4 | +3 hours 35 minutes |
| 5 | +23 hours 35 minutes |
After the fifth attempt we stop, permanently. There is no later automatic redelivery. If you were unavailable for a day, reconcile using section 8.
7.5Deduplication is your responsibility
A retry re-sends the same id and the same occurred_at; only the attempt number and the signature change.
Store id and ignore an event you have already accepted. Otherwise a receiver that fails after successfully processing will create duplicate buyer enquiries.
08Reconciliation
| To recover | Call |
|---|---|
| A listing's current status and publication state | GET /listings/{partner_listing_id} |
| Buyer enquiries you may have missed | GET /listings/{partner_listing_id}/inquiries |
The enquiries endpoint returns every enquiry we attempted to deliver for that listing, failures included — a complete record from our side, not just successful deliveries.
09Onboarding checklist
| # | Step | Owner |
|---|---|---|
| 1 | Agree the slug, expected volumes and environments | Both |
| 2 | Issue API key, signing secret and base URLs | BizScout |
| 3 | Provide the webhook base URL, and a receiver credential if your system needs one | Partner |
| 4 | GET /status returns 200. Note this confirms only connectivity and your key — GET is not signed, so it proves nothing about signing. | Partner |
| 5 | Create a listing, and confirm it reaches published | Partner |
| 6 | Receive a listing.status_changed webhook and verify its signature | Partner |
| 7 | Withdraw that listing — proves a bodyless DELETE is signed correctly | Partner |
| 8 | Redeliver an event and confirm your receiver deduplicates on id | Partner |
| 9 | Confirm strict signature checking is enabled, and re-run steps 5–7 | Both |
Step 9 matters: until it is done, a signing bug on your side can pass every earlier step.
BizScout Partner API · Integration Guide v2.0 · current as of 4 September 2026. Quote a request_id when reporting a problem.