The Warp Notes webhook
Every note your team finishes in a Warp Notes Business workspace can be sent, as it finishes, to a URL you choose: a Zapier or Make catch hook, or your own server. One request per note, carrying the summary and action items, the company, the people, the follow-ups, and who recorded it. Zapier and Make take it from there into HubSpot, Salesforce, Zoho, a Google Sheet, Slack, or anything else they connect to.
Prefer pictures? Every click of the HubSpot and Salesforce setup, on Zapier and on Make, with screenshots: the CRM sync guide.
Using HubSpot or Salesforce? Start from a template. The Zap is already built: the catch hook, a filter that keeps only new notes, lookups that find the contact by email, the company by website domain and the rep who recorded the note by their email, and then the CRM entry itself. Open the Warp Notes → HubSpot template (a Note on the contact, attached to the company, owned by the rep) or the Warp Notes → Salesforce template (a completed Task on the contact, related to the account, assigned to the rep, so it shows in the activity timeline like a logged call), click Try it (Use this template if you are not signed in), connect your CRM when it asks, click through to Review and publish. Then open step 1, copy the webhook URL from its Test tab into the app, and send a test. About five minutes, and nothing to map. On Make instead? Import the HubSpot blueprint or the Salesforce blueprint β the same lookups and the same entries, with the steps under Make.
What "on the right contact" depends on. Both templates match by exact values, never by guessing: the person's email in Warp Notes must equal the CRM contact's email, the company's website domain must equal the CRM company's domain (Salesforce also accepts an exact account name), and the rep's Warp Notes login email must be a CRM user's email. When a lookup finds nothing the entry is still written, just without that link, and it belongs to whoever connected Zapier. So the one thing that makes the promise true is emails on your people, which a CRM export and the app's Import put there in bulk. And "CRM sync ✓" on a note means your link accepted it: if the CRM later rejects it, the failure shows in Zapier's history, not in the app.
Setting it up
- Get a URL that accepts an HTTPS
POST. In Zapier that is a Catch Hook; in Make a Custom webhook; on your own server, any route. To just look at what arrives, a request catcher such as webhook.site gives you one instantly. - In the app, open the workspace, expand Workspace settings, tap CRM sync, tap Zapier, Make or your own server, paste the URL and save. The workspace owner is the only person who can do this; every member can see that it is connected.
- Tap Send a test. Your receiver gets a
pingwithin a second or two β and it is a dress rehearsal: it carries every block a real note carries (note,company,people,follow_ups,recorded_by), built from your most recent note and honouring the Advanced switches, so Zapier and Make learn the full field list from the test alone. A workspace with no notes yet gets clearly labelled sample values. - Record a note. When it finishes, the note shows Sent to your webhook, and your receiver has a
note.created.
Only new notes are sent. Connecting a workspace sends the notes recorded from then on and nothing
older. To send earlier notes, the owner taps Send older notes, picks a window (30 days, 90 days, or
everything), sees the count and the rough Zapier or Make cost, and confirms. Those go out about twenty a
minute and arrive with "history": true. Notes that came in by CSV import are never sent.
Events
| Event | When | Default |
|---|---|---|
ping | The owner taps Send a test. Same blocks as note.created, so it teaches the fields; filter it out before any CRM action. | always |
note.created | A note finishes processing, or the owner sends history. | on |
note.updated | A delivered note's title, summary, action items, company or people change. | off β Advanced β Send changes too |
note.deleted | A delivered note is deleted. | off β same switch |
task.created | A follow-up on a note, as its own request. | off β Advanced β Send each follow-up separately |
task.updated | A delivered follow-up changes or is ticked off. | off β both switches |
task.deleted | A delivered follow-up is deleted. | off β both switches |
Edits and deletions are off by default for a reason: a Zapier or Make catch hook runs its Zap on every
request it receives, so an edit would create a second note in your CRM. Leave them off unless your flow
handles updates. Follow-ups are always inside the note's payload; the separate task.* events exist
for flows that want one request per task.
The request
An HTTPS POST with a JSON body and these headers:
| Header | Value |
|---|---|
Content-Type | application/json |
User-Agent | WarpNotes-Webhook/1 (+https://warpnotes.com) |
X-WarpNotes-Event | The event name, e.g. note.created. |
X-WarpNotes-Version | The payload version, currently 1. |
X-WarpNotes-Delivery | An id for this delivery. A retry repeats it, so you can ignore a duplicate. |
X-WarpNotes-Timestamp | When the request was signed, in Unix seconds. |
X-WarpNotes-Signature | sha256=<hex> β see verifying the signature. |
Redirects are not followed. Answer any 2xx within ten seconds; anything else is a failure and is retried.
The payload
Every request starts with the same envelope, then the blocks for its event.
| Field | Meaning |
|---|---|
event | The event name. |
version | The payload version. See versioning. |
delivery_id | Same as the X-WarpNotes-Delivery header. |
attempt | 1 on the first try, higher on retries. |
history | true when the owner sent history rather than the note being new. |
sent_at | ISO 8601, UTC. |
note.created and note.updated
The full sample: sample-note.created.json. Paste it into Make's Generate from sample or use it to build your Zap's field mapping.
{
"event": "note.created", "version": 1, "delivery_id": "β¦", "attempt": 1, "history": false, "sent_at": "β¦",
"workspace": { "id": "β¦", "name": "Wasatch Supply" },
"note": {
"id": "β¦", "recorded_at": "2026-09-08T18:17:04.000Z", "source": "voice",
"title": "Draper Ridge wants the new rooftop line",
"headline": "Marcus asking about lead times",
"summary": "Marcus is quoting three rooftop units β¦ Warned him about the 8 week window.",
"action_items": ["Confirm rooftop unit lead times with the factory", "Follow up with Marcus on the strip mall bid"],
"transcript": "β¦only when the owner turns on Include the transcriptβ¦",
"captured_location": { "lat": 40.5247, "lng": -111.8638 }
},
"company": {
"id": "β¦", "name": "Draper Ridge HVAC", "domain": "draperridgehvac.com",
"phone": "β¦", "email": "β¦", "website": "β¦", "description": null,
"address": { "text": "1180 E 12300 S, Draper, UT 84020", "lat": 40.5247, "lng": -111.8638 },
"territory": "South", "labels": ["Active", "HVAC"]
},
"people": [
{ "id": "β¦", "name": "Marcus Behl", "title": "Owner", "company": "Draper Ridge HVAC",
"emails": ["marcus@draperridgehvac.com"], "phones": ["+1 801 555 0199"] }
],
"primary_person": { "id": "β¦", "name": "Marcus Behl", "title": "Owner", "company": "Draper Ridge HVAC",
"email": "marcus@draperridgehvac.com", "phone": "+1 801 555 0199" },
"follow_ups": [
{ "id": "β¦", "title": "Confirm rooftop lead times for Marcus",
"due_local": "2026-09-10T11:00:00", "time_zone": "America/Denver",
"all_day": false, "done": false, "completed_at": null }
],
"recorded_by": { "user_id": "β¦", "name": "Jake", "email": "jake@wasatchsupply.example" }
}
note.sourceisvoice,text,uploadorshare.note.transcriptis present only when the owner has turned on Advanced β Include the full transcript. It carries what other people said, which is why it is off by default.note.captured_locationis where the phone was when the note was recorded, when the app had location. Otherwisenull.company.addressandcompany.territoryarenullwhen the company has none.labelsis an array of names, possibly empty.primary_personis the first person on the note, flattened to one email and one phone, so a Zap or a scenario can look the contact up from a single value. Zapier joins every email of every person into one comma-separated string when thepeoplelist is mapped into a plain field, which a CRM lookup never matches; mapprimary_person.emailinstead.nullwhen the note names nobody. People are listed in the order they were linked, so the first one is stable across deliveries.- Follow-up times are what the rep meant, as a local date-time plus the zone it was set in. No UTC instant is invented; combine the two if you need one.
due_localisnullfor an undated to-do. recorded_bynames the team member who recorded the note. For a note that has no account behind it,user_idandemailarenullandnameis whatever the note carries.
task.created and task.updated
Sample: sample-task.created.json. Carries task
(the same shape as an entry in follow_ups), note as { "id", "title" },
company, person (the follow-up's contact, or null) and recorded_by.
note.deleted and task.deleted
{ "event": "note.deleted", "version": 1, "delivery_id": "β¦", "attempt": 1, "history": false, "sent_at": "β¦",
"note": { "id": "β¦" } }
ping
Sample: sample-ping.json. The envelope, then the same blocks as
note.created β built from the workspace's most recent note, transcript included only when that switch is on β
and a message. A workspace with no notes yet gets sample values, each marked "Sample". Because it is shaped
like a real note, always filter on event before a CRM action, or a test would create a record.
Delivery and retries
- Timing. A new note is sent within about a minute of finishing. History drains at about twenty notes a minute, so two thousand notes take under two hours.
- Success. Any
2xxanswered within ten seconds. The body is ignored, though the first part of it is kept so the owner can see what your server said. - Failure. Anything else, a timeout, or a redirect. Retried after 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours, then marked failed on the note, where a tap on Retry queues it again.
- Duplicates. Every retry carries the same
delivery_id. If you store it, you can safely ignore a second arrival. - Order. Not guaranteed. Use
note.recorded_at, not the order requests arrive in. - Paused workspaces. If the workspace's subscription lapses, deliveries wait rather than fail, and resume when it is renewed.
Verifying the signature
Zapier and Make do not check signatures and do not need to. If your own server receives the requests, you can confirm each one came from Warp Notes and was not altered or replayed. When the owner saves the link, the app shows a signing key once, under Advanced; keep it on your server. Every request is signed as:
X-WarpNotes-Signature: sha256=HMAC_SHA256( secret, timestamp + "." + raw_body )
Compute the same value over the raw request body (before any JSON parsing) and compare in constant time. Reject requests whose timestamp is more than a few minutes old to rule out replays.
// Node
import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(rawBody, headers, secret) {
const ts = headers["x-warpnotes-timestamp"];
const given = headers["x-warpnotes-signature"] ?? "";
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false; // 5 minutes
const expected = "sha256=" + createHmac("sha256", secret).update(`${ts}.${rawBody}`).digest("hex");
return given.length === expected.length && timingSafeEqual(Buffer.from(given), Buffer.from(expected));
}
# Python
import hmac, hashlib, time
def verify(raw_body: bytes, headers: dict, secret: str) -> bool:
ts = headers.get("X-WarpNotes-Timestamp", "")
given = headers.get("X-WarpNotes-Signature", "")
if abs(time.time() - float(ts or 0)) > 300:
return False
expected = "sha256=" + hmac.new(secret.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(given, expected)
The owner can rotate the secret from the app at any time; the old one stops working the moment the new one is minted.
Zapier, step by step
Zapier's webhook trigger is a premium app, so it needs a paid Zapier plan. A CRM Zap is about three tasks per note β find the contact, create the note, create the task β which is worth knowing when you pick a plan.
The short way, for HubSpot or Salesforce: open the HubSpot template or the Salesforce template. Each arrives with steps 1, 4 and 5 below already built and the sample already loaded β the catch hook, the filter, the lookups and the CRM entry β so you connect the CRM, publish, then do step 2 with the URL from step 1's Test tab. Anything else you want in the entry is one edit to its body field.
- Create a Zap. For the trigger choose Webhooks by Zapier, event Catch Hook. Leave Pick off a child key empty. Copy the webhook URL Zapier shows.
- In the app: Workspace settings β CRM sync β Zapier, Make or your own server β paste the URL β Save. Then tap Send a test.
- Back in Zapier, click Test trigger. It finds the
ping, which already carries every field a real note does, so you can map from it directly. - Add a Filter by Zapier step: continue only if Event exactly matches
note.created. Without it, the Zap also runs on pings. - Add your actions. For HubSpot, what the template does: Find Company by
company.domainand Get Owner by Email withrecorded_by.email, both set to succeed when nothing is found; then Create Engagement of type Note with the title,note.summaryand the action items as the body, the contact emails (people[].emails[], HubSpot accepts the whole list) as the contact association, the found company's ID as the company association, the found owner's ID as Assigned To, and halt on association error off. For Salesforce, what the template does: Find Record Contact by Email =primary_person.email, Find Record Account by Website =company.websiteOR Name =company.name, Find Record User by Email =recorded_by.email(all three set to succeed when nothing is found); then Create Record of type Task:note.titleas Subject, the summary and action items as Description, Status Completed,note.recorded_atas Due Date, the contact's ID as Name, the account's ID as Related To and the user's ID as Assigned To. For a spreadsheet: Create Spreadsheet Row with the fields you want. Mapnote.recorded_atto any date column, not the time the Zap ran. - Turn the Zap on. From now on every finished note in that workspace runs it.
Zapier shows arrays such as people and follow_ups as numbered fields
(people 1 name, people 2 name) and as comma-separated line items. To create one CRM task per follow-up,
use a Looping by Zapier step over follow_ups, or turn on Advanced β Send each follow-up separately in the app
and give task.created its own Zap.
Make, step by step
Make's free plan can receive the webhook and run a scenario. The blueprints below cost about nine credits per note for HubSpot and ten for Salesforce, one per module; a scenario you build yourself is usually four or five.
The short way, for HubSpot or Salesforce: download the HubSpot blueprint (a Note on the contact, attached to the company, owned by the rep) or the Salesforce blueprint (a completed Task on the contact, related to the account, assigned to the rep, so it shows in the activity timeline like a logged call). Both find the contact by email, the company by website domain and the rep by their email, and a miss leaves that link empty rather than guessing. Both were run against real HubSpot and Salesforce accounts before they were published.
- In Make, click Create a scenario, open the ⋮ menu at the top right, choose Import blueprint and pick the file. Every module and every mapping arrives already set.
- Double-click the first module, Warp Notes webhook, click Create a webhook, save it and copy the address. In the app: Workspace settings β CRM sync β Zapier, Make or your own server β paste the address β Save. Tap Send a test; the scenario ignores tests on purpose, so the app saying it was delivered is enough.
- HubSpot: double-click the second module and click Add next to Connection. Switch on Advanced settings and add five Custom scopes: CRM Companies Read, CRM Companies Write, CRM Contacts Read, CRM Contacts Write and CRM Owners Read. A Make HubSpot connection made with the defaults has none of them, and the modules then fail with "hasn't been granted all required scopes". If HubSpot answers the sign-in with "this app hasn't been approved to access the required scopes", a HubSpot super admin allows them once: in HubSpot, Settings β Integrations β Connected apps β Approved apps β Make β Manage access β Data Permissions, switch on companies read, companies write, contacts read and owners read, Save changes, then save the Make connection again. Open each of the other HubSpot modules, pick the new connection, click Save.
- Salesforce: double-click the second module, click Add next to Connection, keep the type Salesforce, sign in and save. Open each of the other Salesforce modules, pick the connection, click Save. Salesforce needs API access on your edition (Professional sells it as an add-on).
- Save the scenario. Under scheduling choose Immediately (an import sets it to every 15 minutes), then switch the scenario on. Record a note; it reaches the CRM within a minute.
Building your own instead: add the Webhooks β Custom webhook module, click Add, name it, save,
and paste its address into the app as above. Make shows Determine data structure: tap Send a test in the
app β the test carries every field a real note does β or click Generate and paste
sample-note.created.json. Add a Filter after the webhook,
event equal to note.created, then your modules; Make handles arrays natively, so an Iterator
on follow_ups creates one CRM task per follow-up. Two things the blueprints learned the hard way: a Make search
module that finds nothing ends the route, which is why they use each app's Make an API Call module (it always
answers exactly once), and Make does not escape text you map into a JSON body, which is why they pass the note through
JSON β Transform to JSON first. Set the schedule to Immediately and turn the scenario on.
Versioning
The version field and the X-WarpNotes-Version header are 1. New fields may be
added to any block at any time without changing the version; build your mapping on the fields you use and ignore
the rest. The version changes only if an existing field is renamed, removed, or changes meaning, and that will be
announced here first with both versions available for a transition period.
Privacy
The workspace owner chooses the destination, can disconnect it at any time, and every member can see that it is connected. What your receiving service does with the data is governed by that service's terms. The details are in the privacy policy under Sending notes to a service you choose.
Questions, or a destination you would like supported natively: Jacob.Metcalf@appomatix.com.