The Marketplace API is in a private beta. It answers only for accounts Investorlift has enabled. Access says how to ask for one. A route shape on these pages can change before the beta ends. The changelog records every change.
Endpoints
Create an endpoint withPOST /sell/webhooks or POST /buy/webhooks under the scope webhooks:manage. On the sell
side, an owner or an admin creates it. The body names the URL and the event types. Each side holds ten endpoints, and
the eleventh answers 409 webhook_limit.
GET lists the endpoints with the creator, the client and the last delivery. PATCH /sell/webhooks/{webhook_id}
changes the event types, pauses the endpoint or starts it again. rotate_secret: true in the same body mints a new
signing secret, shown once. DELETE removes one. The /buy/ side has the same routes.
POST /sell/webhooks/{webhook_id}/test sends a ping event to that endpoint alone, and
POST /buy/webhooks/{webhook_id}/test does the same on the buy side. The test answers 202 with
data: { endpoint_id, event_id, type }, and type reads ping. Read the delivery in last_delivery on the next read
of the endpoint.
- The URL is
httpsonly, on a public host, with no user name or password. The API resolves the host at create and again before every delivery. A host that resolves to a private, loopback, link-local or metadata address answers422 webhook_url_refusedat create, and takes no delivery later. The API follows no redirect: a3xxis a failed delivery. - Before the endpoint is
active, the API sends a challenge to the URL and expects it echoed. Until then the endpoint readspending_verification. The other states arepausedanddisabled. - The create response shows the signing secret once. It starts with
whsec_. Store it. Every later read shows its last four characters only. The secret rotates throughPATCHwithrotate_secret: true. During a rotation a delivery carries the old and the new signature. - The endpoint pins
api_versionat create. Every delivery to it renders the event at that version. A payload change ships as a new version, and you opt into it with a new endpoint. - The endpoint records its creator and client. When the creator leaves the organization or revokes your application, the endpoint pauses. The owner of the organization then gets an email to confirm or delete it within 7 days.
- A paused endpoint names the cause in
paused_reason:creator_left,consent_revokedorclient_disabled. An owner or an admin starts it again withstatusset toactive.
The signed delivery
Every delivery is aPOST with the content type application/json, the User-Agent
Investorlift-Marketplace-Webhooks/1, and the three headers of the Standard Webhooks specification:
Verify before you parse. Compute the HMAC over the exact bytes of the body. Compare with a constant-time comparison.
Refuse a timestamp more than 5 minutes from your clock. Answer
2xx inside 15 seconds, and do the work after.
Delivery
Delivery is at least once and unordered. The event id is your dedupe key: store it, and skip a delivery whose id you hold. A delivery that gets no2xx inside 15 seconds is a failure, and so is a 3xx. The API retries a failure five times over about
four minutes with backoff, then once an hour. An endpoint that fails for 3 days is disabled: the API emails the owner
and emits webhook.disabled. The API keeps a payload 7 days for redelivery, then keeps its hash.
The event shape
One event has one shape on the feed and on a webhook:The catalogue
The event types are a closed list for each side. An endpoint takes the types of its own side and the two compliance types, and no other type. The OpenAPI document of this API lists each event type in itswebhooks section, with the
three headers and the event shape.
The sell side. The owner is the organization.
The buy side. The owner is the buyer.
Compliance, both sides.
buyer.redacted and lead.redacted fire on account deletion or a verified deletion request.
Each carries one id and nothing else. On buyer.redacted, delete the buyer’s contact from your systems within 10
business days and keep the id as a tombstone (Terms).
deal.matched carries deal_id, city, state, zip, asking_price, deal_type and matched_buy_box_id, and no
other deal field. The full read is GET /buy/deals/{deal_id} against the deal budget, and the delivery itself counts
one deal row.
What an event says about a buyer
A seller-side event about an offer, an inquiry, an address request or a lead carriesbuyer: { buyer_id, name, entity_name }. The fields email, phone (one number) and phone_type ride only when the
creator of the endpoint holds contacts:read, an owner or an admin. On the feed, the token that reads must hold it. A
token without it gets no such key: absent, never null. Each row with email or phone counts against the daily
contact-bearing row cap (Trust and limits). A delivery past
that cap carries no contact members, and data.contact.status reads budget_reached.
No event carries a score, a rank, a breakdown or a channel of a recommended lead. lead.created carries a consent
block with source, captured_at, text_version and sms_opt_in. You are the sender under the TCPA and CAN-SPAM for
anything you do with the contact (Terms). A new organization is one with fewer than five verified
deals or under 90 days of standing. It gets no email or phone on an API-published deal until Investorlift verifies
the deal. Such a row carries contact: { status: "pending_verification" }.
The events feed
GET /sell/events and GET /buy/events serve the events of the side under the scope events:read, in sequence
order, in the shape above. The query takes four parameters:
cursor=, thepage.next_cursorof the page before this onetypes=, a comma list of event typesfrom=, an RFC 3339 instant for a first readlimit=, the page size, 1 to 100, 50 by default
types= and keep the cursor. A cursor from another account answers
400 invalid_parameter. Read the feed again without a cursor.
Replay
1
Store the id and the instant of every event you handle
From a webhook, store
id and occurred_at from the body. From the feed, also store page.next_cursor.2
Poll the feed with the cursor
Send
GET /sell/events?cursor=... from your last cursor. Handle every event whose id you do not hold. Follow
next_cursor until it is null.3
After a gap or an outage, start from an instant
Send
from= with the occurred_at of your last handled event, inside 30 days. Skip the ids you hold. Beyond 30 days,
read the resources with updated_since= instead.