Contents

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.

DirectionWhat travelsHow
You → BizScoutListings: create, update, withdraw. Optionally status and deal events.You call our REST API
BizScout → YouBuyer enquiries; listing status changesWe POST to a receiver you host

The happy path

  1. POST /listings. We answer 201 with publication_state: "queued".
  2. We publish it automatically, normally within seconds.
  3. We POST listing.status_changed with publication_state: "published". A GET now returns a public_url.
  4. A buyer enquires. We POST buyer_inquiry.created with their contact details.
  5. When the business sells, tell us — PATCH to SOLD, 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 youPurpose
Partner slugYour identifier, and a path segment in every URL.
API keySent as X-API-Key on every request.
Signing secretUsed both to sign your requests to us and to verify the signature on the webhooks we send you.
Base URL per environmentIssued at onboarding.
You give usNotes
Webhook base URLA 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 volumesSo 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/listings

How 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 gatewayRefused by the API
StatusAlways 403400, 403 or 404
BodyExactly {"message":"Forbidden"}{"error":{"code","message","request_id"}}
Has a request_id?NoYes
CauseAPI 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.
Debugging rule

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.

Signing is a per-partner setting

Sign every request that changes somethingPOST, 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.

HeaderValueRequired on
X-API-KeyThe key we issuedEvery request
X-BizScout-SignatureSee belowEvery request except GET, HEAD, OPTIONS
Content-Typeapplication/jsonEvery request with a body

A request that fails authentication returns 403 with a deliberately uninformative message.

Easy to miss

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:

  1. 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.
  2. A request with no body signs the empty string — not {}, not null. This applies to DELETE, which is not exempt.
  3. 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

CodeMeaning
200Success. Also a replayed POST /listings for a listing we already hold.
201Listing created.
202Accepted for asynchronous processing (batch, events).
204Listing withdrawn.
400Validation failure, or an unrecognised field.
403Authentication failure, or a suspended account.
404Unknown partner_listing_id or batch id.
413Body over 10 MB.
500 / 503Our 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" }
    ]
  }
}
FieldNotes
codeOne of bad_request, forbidden, not_found, internal_error.
messageHuman-readable. For validation failures this is always the literal "Validation failed" — the detail is in details.
request_idQuote 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.
detailsPresent only for field-level validation failures. Omitted otherwise.

4.3Idempotency

  • POST /listings is keyed on partner_listing_id. A repeat returns 200 with the listing as it already exists — the replayed body is discarded. This is not an upsert; use PATCH to 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 new event_id for a retry.

4.4Sending null

Read before writing your client

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. 1250000 means $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.

MethodPathPurposeSuccess
POST/listingsCreate a listing201, or 200 on replay
POST/listings/batchCreate many, asynchronously202
GET/listings/batch/{batch_id}Poll a batch200
GET/listings/{partner_listing_id}Read current state200
PATCH/listings/{partner_listing_id}Partial update200
DELETE/listings/{partner_listing_id}Withdraw (reversible)204
GET/listings/{partner_listing_id}/inquiriesReplay buyer enquiries200
POST/listing-eventsPush a status change (optional)202
POST/deal-eventsPush a deal stage change (optional)202
GET/statusConnectivity check200
POST/listings

Create one listing. This endpoint defines the listing shape — PATCH and each item of a batch accept the same fields.

Required

FieldTypeNotes
partner_listing_idstring, max 128Your identifier, and the listing's identity for the whole integration. Every other call references it. Must be stable forever.
titlestring, max 200Non-empty.
descriptionstringNon-empty. No maximum.
industrystringFree text, non-empty. Not validated against a list.
listing_statusenumACTIVE · UNDER_LOI · SOLD · WITHDRAWN. Case-sensitive. See section 6 for what each one does.

Five fields. Everything below is optional.

Optional — identity and headline

FieldTypeNotes
urlstring, max 2048Your listing's page. Not required, not unique — two listings may share one.
websitestring, max 2048The business's own site.
headlinestringShort summary line.
metadataobjectFree-form. Stored and echoed back unchanged — use it for your own identifiers.

Optional — financials

All whole dollars, no decimals, no currency field.

FieldTypeNotes
asking_priceintegerWhat the business is listed at.
cash_flowintegerSeller's discretionary earnings.
gross_revenueintegerAnnual revenue.
ebitdainteger
inventoryintegerValue of stock included in the sale.
rentintegerMonthly rent.

Optional — location

FieldTypeNotes
detailed_locationstringHuman-readable, e.g. "Phoenix, AZ". We geocode it.
locationobject{ "lat": number, "lng": number } — both required if present, lat −90 to 90, lng −180 to 180. Supplying it skips our geocoding.

Optional — broker contact

FieldTypeNotes
broker_namestring
broker_phonestringNo format validation.
broker_emailstringMust be a valid email address.
Public display

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

FieldTypeNotes
imagesarray of stringMax 25. Every entry must be a well-formed URL — stricter than url itself.
establishedstringFree text, not a date. "2008" is typical.
detailed_employeesintegerHeadcount.
detailed_building_sfstringA string, not a number.
detailed_real_estatestringFree text.
detailed_lease_expirationstringFree text, not a date.
detailed_facilitiesstringFree text.
detailed_competitionstringFree text.
detailed_growth_expansionstringFree text.
detailed_support_trainingstringFree text.
detailed_reason_for_sellingstringFree 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 fieldNotes
bizscout_listing_idOur id, as a string. Store it — every webhook carries it.
publication_statequeued · published · unpublished. Ours, not yours — see section 6.
public_urlPresent only while publication_state is published, so never on a create response.
Everything you sentEchoed 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.

POST/listings/batch

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_id within 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.

GET/listings/batch/{batch_id}

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" } }
  ]
}
FieldValues
statusqueued · processing · completed · failed
summary, resultsPresent once completed.
results[].statuscreated · 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.

GET/listings/{partner_listing_id}

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_url while published.
Errors
404 unknown partner_listing_id.
Not obvious

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.

PATCH/listings/{partner_listing_id}

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_id in 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.

DELETE/listings/{partner_listing_id}

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_status becomes WITHDRAWN and 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.

GET/listings/{partner_listing_id}/inquiries

Replay the buyer enquiries we have sent, or attempted to send, for this listing. Your catch-up path if your receiver was unavailable.

QueryTypeNotes
limitinteger1–100. Default 25.
cursorstringOpaque. Pass back the previous page's next_cursor.
created_afterstringMust be a valid ISO-8601 timestamp.
{ "data": [ { …enquiry… } ], "has_more": true, "next_cursor": "18240" }
Response
Each element of data is the data block of a buyer_inquiry.created event, identical to what the webhook carried — see 7.3.
Errors
400 limit out of range · 404 unknown listing.
POST/listing-eventsoptional

Push a status change as an event instead of a PATCH. Most integrations use PATCH.

FieldTypeRequiredNotes
event_idstring, max 128YesYour id for this event. The deduplication key — never regenerate it for a retry.
event_typeliteralYesOnly listing.status_changed is accepted.
partner_listing_idstringYesThe listing this is about.
statusenumYesACTIVE · UNDER_LOI · SOLD · WITHDRAWN.
occurred_atstringYesISO-8601. Recorded for audit — arrival order decides, not this value.
previous_statusenumNoRecorded, not used for resolution.
reasonstringNoFree text.
metadataobjectNoFree-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_id returns the original acknowledgement and is not applied twice.
Errors
400 validation · 404 unknown partner_listing_id.
Ordering matters

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.

POST/deal-eventsoptional

Push a deal stage change.

FieldTypeRequiredNotes
event_idstring, max 128YesDeduplication key.
event_typeliteralYesOnly deal.stage_changed is accepted.
partner_listing_idstringYesThe listing the deal is on.
partner_deal_idstringYesYour id for the deal. Multiple deals may sit on one listing.
stagestringYesOpen 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_atstringYesISO-8601. Arrival order decides.
previous_stagestringNoFree text.
amountintegerNoWhole dollars.
bizscout_inquiry_idstringNoThe inquiry_id we sent you, to join the deal back to the buyer enquiry.
brokerobjectNo{ name, email, phone, firm } — all optional; email must be valid if present.
buyerobjectNoSame shape as broker.
metadataobjectNoFree-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.
One stage has side effects

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

Fails silently

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.

GET/status

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.

ValueOwnerMeaning
listing_statusYouWhere the business is in its sale: ACTIVE, UNDER_LOI, SOLD, WITHDRAWN.
publication_stateBizScoutWhether we are showing it: queued, published, unpublished.
You set listing_status toResult
ACTIVE, from a non-active statequeued, then published — back on the marketplace
UNDER_LOI, SOLD or WITHDRAWNunpublished — leaves the marketplace
Not obvious

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 publication stalls

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:

PathEvent
<your_base>/inquirybuyer_inquiry.created
<your_base>/status-changelisting.status_changed

Route on the path, or on the event field in the body — both are reliable.

7.1Headers on every delivery

HeaderValue
Content-Typeapplication/json
X-BizScout-EventThe event type.
X-BizScout-Delivery<envelope_id>:<attempt_number> — how you tell a retry from a first attempt.
X-BizScout-SignatureVerify it as in 3.2.
Your credential headerIf you gave us a credential at onboarding, we send it on every delivery in the header your system expects.

7.2Envelope

FieldTypeNotes
idstringevt_<uuid>. Your deduplication key — stable across every retry.
eventenumlisting.status_changed or buyer_inquiry.created.
api_versionstringv1.
occurred_atstringISO-8601. Stamped once and stable across retries.
dataobjectPer-event. See below — the two events differ.

7.3Payloads

This catches people

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 fieldNotes
bizscout_listing_idOur id, as a string.
partner_listing_idYours. Use it to find your record.
publication_stateThe new state.
listing_statusThe new status. Carried alongside the publication state, so one event tells you the whole picture — including “left the marketplace because it sold”.
changed_atISO-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 fieldNotes
inquiry_idOur id for the enquiry. Send it back on a deal event to join the deal to the lead.
buyer.first_name, last_name, email, phoneAlways present.
buyer.roleThe buyer's BizScout subscription tier, free or pro. The key is omitted entirely when we cannot determine it — absent means unknown, not free.
listingOnly the two identifiers. No listing name, location or broker details — look those up from your own record using partner_listing_id.
purchase_timeframeWhat the buyer told us, e.g. "0-3 months".
messageThe buyer's message. An empty string when they left none — never null.
created_atISO-8601.

7.4Retries

Answer 2xx as soon as you have durably accepted the event, then process. Each request times out after 10 seconds.

You answerWe
Any 2xxMark it delivered.
403Retry — we assume transient auth trouble and re-sign each attempt.
Any other 4xxStop. The same bytes cannot become acceptable.
5xx, timeout, connection errorRetry.
AttemptWhen
1immediately
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.

Most important thing on your side

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 recoverCall
A listing's current status and publication stateGET /listings/{partner_listing_id}
Buyer enquiries you may have missedGET /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

#StepOwner
1Agree the slug, expected volumes and environmentsBoth
2Issue API key, signing secret and base URLsBizScout
3Provide the webhook base URL, and a receiver credential if your system needs onePartner
4GET /status returns 200. Note this confirms only connectivity and your key — GET is not signed, so it proves nothing about signing.Partner
5Create a listing, and confirm it reaches publishedPartner
6Receive a listing.status_changed webhook and verify its signaturePartner
7Withdraw that listing — proves a bodyless DELETE is signed correctlyPartner
8Redeliver an event and confirm your receiver deduplicates on idPartner
9Confirm strict signature checking is enabled, and re-run steps 5–7Both

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.