Help / API and webhooks
API and webhooks
Connect your own tools to DealWolf: read your deals, add a lead from a form or another system, and get a signed message the moment a deal is added or moves stage, an offer changes status, a saved search finds new matches or a pack is made. It works on your own workspace only. Using Zapier or Make? See Zapier and Make. The whole API is also described as an OpenAPI 3.1 spec. It finds, qualifies, drafts and reports: it never contacts a seller, agent or investor, and nothing here sends anything to them.
1. Make a key
The workspace owner makes keys on the API and webhooks page (from Account), and names each one. A key is shown once; DealWolf keeps only a fingerprint of it, so a lost key is replaced (Make a new key), not recovered. Revoking a key stops it at once. Give each tool its own key and only the access it needs. A key is either:
- Read: Read only: deals, buyers and events.
- Read and write: Read and write: also add leads and subscribe webhooks.
Underneath, each route checks one scope:
read-deals: Read dealswrite-deals: Create deals and leadsread-buyers: Read buyers (names and buy boxes only)read-events: Read recent events (for Zapier and Make polling)write-webhooks: Subscribe and unsubscribe webhooks (Zapier and Make instant triggers)
Each key has its own limit of 60 requests a minute and 10,000 a month by default, and the whole workspace has 300 a minute across all its keys. Every answer carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset (seconds). Over a limit you get a 429 with Retry-After. API calls spend no credits.
export DEALWOLF_URL="https://your-dealwolf-address"
export DEALWOLF_KEY="dwk_..." # from Account, never put it in a web page or app you hand outSend the key as a bearer token on every request, over https:
curl -s "$DEALWOLF_URL/api/v1/deals?limit=5" \
-H "Authorization: Bearer $DEALWOLF_KEY"2. List and read deals
GET /api/v1/deals needs read-deals. Newest first, up to 100 a page (default 25). Filters: stage, market (UK or US) and strategy. When there are more, has_more is true and next_cursor goes in the next request as cursor; the answer also has a Link: <…>; rel="next" header with the next page's address. Every list in the API pages this way.
curl -s "$DEALWOLF_URL/api/v1/deals?stage=offer&market=UK&limit=2" \
-H "Authorization: Bearer $DEALWOLF_KEY"
curl -s "$DEALWOLF_URL/api/v1/deals?stage=offer&market=UK&limit=2&cursor=NEXT_CURSOR" \
-H "Authorization: Bearer $DEALWOLF_KEY"{
"data": [
{
"id": "0b9d6a3e-6f1c-4d63-9d3c-6a6f0d0c9c11",
"title": "12 Station Road",
"address": "12 Station Road, Slough",
"postcode": "SL1 3AB",
"market": "UK",
"strategy": "btl",
"stage": { "id": "offer", "label": "Offer" },
"asking_price": {
"status": "given", "amount": 185000, "currency": "GBP",
"certainty": "entered",
"basis": "figure entered on the deal in your workspace",
"as_of": "2026-11-05"
},
"verdict": { "status": "needed" },
"bedrooms": 3,
"property_type": "terraced",
"listing_url": null,
"next_action": { "text": "Call the agent", "due": "2026-11-07" },
"contact": { "name": "A. Agent", "company": null, "phone": "01753 000000", "kind": "agent" },
"created_at": "2026-11-01T09:00:00.000Z",
"updated_at": "2026-11-05T09:00:00.000Z"
}
],
"has_more": true,
"next_cursor": "MjAyNi0x..."
}Every figure says what it is: a certainty, its basis and the date it is as_of. A figure nobody has given yet is { "status": "needed" }, never a guess. UK deals are in pounds (GBP) with a UK postcode; US deals are in dollars (USD) with a 5-digit ZIP. The two are never mixed.
curl -s "$DEALWOLF_URL/api/v1/deals/0b9d6a3e-6f1c-4d63-9d3c-6a6f0d0c9c11" \
-H "Authorization: Bearer $DEALWOLF_KEY"3. Add a lead
POST /api/v1/deals needs write-deals. The lead arrives unassigned at the New stage, with a next step "Look into this lead", and no verdict until someone stacks it. Give a title or an address; the rest is optional: market (your workspace's home market if left out), postcode (UK) or ZIP (US), strategy, asking_price (a number, in the market's money), bedrooms, listing_url (https), contact_name and contact_phone. Unknown fields are refused.
curl -s -X POST "$DEALWOLF_URL/api/v1/deals" \
-H "Authorization: Bearer $DEALWOLF_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: web-form-8841" \
-d '{
"address": "12 Station Road, Slough",
"postcode": "SL1 3AB",
"market": "UK",
"asking_price": 185000,
"bedrooms": 3,
"contact_name": "A. Seller"
}'curl -s -X POST "$DEALWOLF_URL/api/v1/deals" \
-H "Authorization: Bearer $DEALWOLF_KEY" \
-H "Content-Type: application/json" \
-d '{
"address": "88 Oak Lane, Charlotte, NC",
"postcode": "28202",
"market": "US",
"asking_price": 240000
}'The answer is 201 with the new deal and a Location header.
Idempotency. Add an Idempotency-Key header (any unique text up to 255 characters, such as your own record id) and a retry of the same request, after a dropped connection say, returns the first answer with Idempotent-Replayed: true instead of making a second lead. The same key with a different body is refused (422). A key is remembered for 24 hours.
4. Buyers (read-only)
GET /api/v1/buyers needs read-buyers and returns each buyer's name, company, kind, status, strategies, areas and budget range, paged the same way. Email, phone, notes, proof of funds and checks are never included, and buyers marked do-not-contact are left out.
curl -s "$DEALWOLF_URL/api/v1/buyers?limit=10" \
-H "Authorization: Bearer $DEALWOLF_KEY"5. Errors
Every error has the same shape:
{
"error": {
"code": "invalid_request",
"message": "Some of the request is not valid.",
"request_id": "req_Qm3x...",
"details": [{ "path": "postcode", "message": "Use a full UK postcode, such as SL1 3AB." }]
}
}| Status | code | Meaning |
|---|---|---|
| 400 | bad_json | The body is not JSON, or not sent as application/json. |
| 401 | unauthorized | No key, a wrong key, or a revoked one. |
| 403 | forbidden | The key lacks the scope this needs. |
| 404 | not_found | No such deal or webhook in your workspace, or no such API address. |
| 405 | method_not_allowed | That address doesn't answer that method; the Allow header says which do. |
| 409 | idempotency_conflict | The same Idempotency-Key is still being processed. |
| 413 | payload_too_large | The body is over 16 KB. |
| 422 | invalid_request | A field is wrong; see details. Also idempotency_key_reused. |
| 429 | rate_limited, quota_exceeded | Slow down (Retry-After says how long), or the month's requests are used. |
| 503 | unavailable | Not available right now; try again. |
Quote the request_id (also the X-Request-Id header) if you ask us about a call.
6. Webhooks
The owner adds an https address on the API and webhooks page and picks the events, or a tool subscribes one itself with POST /api/v1/webhooks (key scope write-webhooks; see Zapier and Make). The signing secret is shown once. Send a test posts a webhook.test event to one address, and the delivery log shows every try and lets you try a failed one again. Events:
deal.created: A deal or lead was added to the workspace: by hand, from the map, a bulk stack, the seller form or the API. Carries the deal (no seller or agent contact details).deal.stage_changed: A deal moved to another pipeline stage (by hand, in bulk, or when a pack moved it to Packed). Carries the deal and the stage it came from.offer.status_changed: An offer in the offer tracker changed status: countered, accepted, rejected, dead or withdrawn. Carries the offer's status before and after, its amount and the last counter. The MAO, walk-away floor and who it went to are never sent.lead.matched: A saved search (buy box) found new homes that match it. Carries the saved search and up to ten homes (address, price, beds, type, map position, whether it scores strong). Which listing site a home came from is never sent.pack.created: A deal pack was made for a deal, from a template or the pack builder. Carries the pack's name and file name, and the deal. A draft: nothing is sent to an investor.
{
"id": "evt_3f0c...",
"type": "deal.stage_changed",
"api_version": "v1",
"created_at": "2026-11-05T10:15:00.000Z",
"data": {
"deal": { "id": "0b9d6a3e-...", "title": "12 Station Road", "stage": { "id": "offer", "label": "Offer" }, "...": "same shape as the API, without contact details" },
"previous_stage": { "id": "viewing_booked", "label": "Viewing booked" }
}
}Contact details (a seller's, agent's or buyer's name, phone or email) are never sent in a webhook; fetch the deal with your key if you need them. Nor is which listing site or data service a home came from, or an offer's MAO or walk-away. Every event of the last 30 days can also be read with GET /api/v1/events (scope read-events), and a made-up example of each with GET /api/v1/events/samples. Headers: DealWolf-Event, DealWolf-Event-Id (the same on every retry: use it to ignore repeats), DealWolf-Delivery and DealWolf-Signature.
Check the signature. The header looks like t=1762337700,v1=9f2c.... v1 is the hex HMAC-SHA256, with your signing secret, of the timestamp, a full stop, and the raw request body exactly as received. Compare in constant time and refuse anything whose timestamp is more than 5 minutes old.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(secret, rawBody, header, nowSeconds = Date.now() / 1000) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const t = Number(parts.t);
if (!Number.isInteger(t) || Math.abs(nowSeconds - t) > 300) return false;
const want = createHmac("sha256", secret).update(t + "." + rawBody).digest();
const have = Buffer.from(parts.v1 ?? "", "hex");
return have.length === want.length && timingSafeEqual(have, want);
}BODY='{"id":"evt_..."}'
T=$(date +%s)
printf '%s.%s' "$T" "$BODY" | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET"Delivery rules.
- Reply with any
2xxwithin 5 seconds. Anything else, or no answer, is a failure. - Failures are tried again after 1, 5, 30, 120, 360 minutes (six tries in all, about eight hours), and each try is recorded in the delivery log. Answer
410to unsubscribe: it is not tried again, and the address is switched off (or removed, if a tool subscribed it). After 20 messages in a row that could not be delivered, the address is switched off; the owner can switch it back on. - Only public https addresses on port 443 are called. Addresses that are, or resolve to, private, loopback or link-local networks are refused, and redirects are never followed.
- A message is at most 32 KB. Your reply is not read beyond the first couple of kilobytes.
- Delivery can arrive twice or out of order. Use
DealWolf-Event-Idto ignore repeats and the deal'supdated_atto order changes.
7. Versions
The address starts /api/v1. New optional fields may be added to answers without a new version, so ignore fields you do not know. Anything that would break your code gets /api/v2 alongside, with notice here first. Every answer carries DealWolf-API-Version: v1.