Skip to content
On this site

Webhooks

Updated

Webhooks is coming in Wisply 1.11. Check the App Store for the version on your device.

Overview

Wisply can post a finished memo that has a transcript as JSON to a URL you choose, typically a Zapier, Make or n8n "catch hook" that passes it on to Linear, Jira, Asana, ClickUp, Google Docs, Notion, a CRM, or anything else those services reach. Your own HTTPS server works too.

  • Off by default. Nothing is sent until you paste a URL and turn it on.
  • Device to your URL only. The request goes directly from your iPhone, iPad or Mac to the URL you entered. There's no Wisply server in between, so Wisply's servers never see the event.
  • Signed. Every request carries Standard Webhooks (opens in a new tab) headers, so a receiver can check it came from your device and wasn't changed.
  • HTTPS only. http:// URLs, URLs with a user name or password, and redirects are refused.
  • The URL and the signing secret are stored in your device's Keychain.

Turn it on

  1. Open Settings → Integrations (on iPhone and iPad: Settings → Integrations → Webhooks).
  2. Paste your webhook URL. An inline message explains anything it rejects.
  3. Optional: turn off Include full transcript to send the title, summary, action items and questions only.
  4. Tap Send test event. The status line shows the HTTP result.
  5. Turn on Send finished recordings to a webhook (Send finished memos on iPhone and iPad).

From then on, each recording, import (an audio or video file, or text you paste on iPhone or iPad) and Apple Watch memo that has a transcript is sent after its title is generated and, when that's on, its Obsidian note is written. A pasted-text memo's transcript is the text you pasted, so that text is sent in transcript and markdown unless Include full transcript is off; its title, which can be the text's first line, and its AI summary are sent either way. A memo with no transcript isn't sent.

If the memo has no summary yet, Wisply first writes one on your device, under the same Wisply Pro and free-allowance rules as the Summary tab.

Delivery is at-least-once: if the app stops after the receiver accepted the event and before Wisply recorded that success, the same memo can be sent again. Receivers should dedupe on the event id, which is the memo id.

Events

Event types
TypeWhen
memo.completedA recording, import (file or pasted text) or Apple Watch memo finished and was saved.
webhook.testYou tapped Send test event. The data is a sample memo with every field filled in, so Zapier, Make and n8n can learn the fields before your first real memo.
Example memo.completed event
{
  "type": "memo.completed",
  "id": "9C1B6A0E-5D1F-4E0B-8F0A-6B2D3C4E5F60",
  "timestamp": "2026-10-19T16:20:05Z",
  "api_version": "2026-10-01",
  "data": {
    "memo_id": "9C1B6A0E-5D1F-4E0B-8F0A-6B2D3C4E5F60",
    "title": "Launch review",
    "created_at": "2026-10-19T15:58:41Z",
    "duration_seconds": 1284.5,
    "language_code": "en",
    "summary": "The team approved the phased launch…",
    "action_items": [
      {
        "owner": "Alex",
        "task": "Publish the beta build",
        "due_date": "Friday"
      }
    ],
    "open_questions": [
      {
        "asked_by": "Sam",
        "question": "Who owns support during launch week?",
        "answered": false
      }
    ],
    "speakers": [
      "Alex",
      "Sam"
    ],
    "transcript": "…",
    "markdown": "# Launch review\n\n*Recorded …*\n\n## Summary\n\n…",
    "source": "wisply",
    "app_version": "1.11.0"
  }
}
Fields
FieldNotes
typememo.completed or webhook.test.
idFor memo.completed, the memo id. Delivery is at-least-once, so receivers should dedupe on this id. A test event uses a new id. Also sent as the webhook-id header and unchanged across the retries of one send.
timestampWhen the event was created. ISO 8601, UTC.
api_versionChanges only when a field is removed or changes meaning. New fields can be added at any time.
dataThe memo.
data.memo_idThe memo's id.
data.titleThe memo's title.
data.created_atWhen the memo was created. ISO 8601, UTC.
data.duration_secondsLength of the recording in seconds.
data.language_codeAbsent when unknown.
data.summaryAbsent when the memo has no summary (for example, Apple Intelligence is unavailable). Also absent when Include full transcript is off and the only summary is transcript excerpts.
data.action_itemsAI action items, possibly empty.
data.action_items[].ownerWho the item is for, as the summary names them. Can be empty.
data.action_items[].taskWhat needs doing.
data.action_items[].due_dateAbsent when no due date was mentioned.
data.open_questionsQuestions raised in the recording, possibly empty.
data.open_questions[].asked_byWho asked, as the summary names them.
data.open_questions[].questionThe question.
data.open_questions[].answeredWhether the recording answered it.
data.speakersDisplay names in order of first appearance; empty without speaker identification.
data.transcriptPlain text. Omitted when Include full transcript is off.
data.markdownThe Obsidian note without YAML frontmatter. The transcript section, the standout quote and an excerpt-based summary are left out when Include full transcript is off.
data.sourceAlways wisply.
data.app_versionThe Wisply version that sent the event.

Audio is never sent.

Requests, retries and timeouts

Each delivery is a POST with a 30-second timeout and these headers:

Request headers
HeaderValue
Content-Typeapplication/json
User-AgentWisply/<version>
webhook-idThe event id.
webhook-timestampUnix seconds of this attempt.
webhook-signaturev1,<base64 HMAC-SHA256>, signed with the secret current at that attempt.
  • Any 2xx response is success.
  • After a network error, a 429 or a 5xx, Wisply tries again, up to 3 attempts in all, waiting 2 seconds and then 8 seconds.
  • Any other status, including a 3xx redirect, is final. Redirects are never followed.
  • Each attempt is signed with the secret current at that moment.
  • A failed delivery is shown in Settings and isn't queued. The same memo can be sent again later, with the same event id.

Before each attempt, a finished memo is sent only if webhooks are still on, and only to the URL configured at that moment. An event that includes a transcript is sent only if Include full transcript is still on. Turning either off, or changing the URL, drops the remaining attempts, and Wisply asks the request in flight to stop, checking at least every half second. That can't recall a request the endpoint has already received, so a request that was already sent may still arrive.

After a 2xx, Wisply remembers the delivered memo in a bounded list, so a later launch usually doesn't send it again; a crash between the 2xx and that write sends it again. Treat delivery as at-least-once and dedupe on the event id.

Wisply reads at most 64 KB of the response body and uses only the status code.

Verify the signature

The signature is HMAC-SHA256 over <webhook-id>.<webhook-timestamp>.<raw body>. The key is your signing secret (copy it from Signing secret in Settings) with the whsec_ prefix removed, base64-decoded. Compare against the raw request body, before any JSON parsing, and reject timestamps more than a few minutes old.

Regenerating the secret replaces it, so update your receiver afterwards. If Wisply can't read or save the secret in the Keychain, the previous secret stays in place: the delivery fails, or Settings shows the error next to Regenerate.

The Standard Webhooks (opens in a new tab) reference libraries verify these headers directly. Or use one of these:

Node.js
const crypto = require("crypto");

function verifyWisplyWebhook(secret, headers, rawBody, toleranceSeconds = 300) {
  const id = headers["webhook-id"];
  const timestamp = headers["webhook-timestamp"];
  const signatures = headers["webhook-signature"];
  if (!id || !timestamp || !signatures) return false;
  const sentAt = Number(timestamp);
  if (!Number.isFinite(sentAt) || Math.abs(Date.now() / 1000 - sentAt) > toleranceSeconds) return false;

  const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
  const expected = Buffer.from(
    crypto.createHmac("sha256", key).update(`${id}.${timestamp}.${rawBody}`).digest("base64")
  );

  return signatures.split(" ").some((entry) => {
    const [version, signature] = entry.split(",");
    if (version !== "v1" || !signature) return false;
    // Compare bytes, not characters: a non-ASCII header has more bytes than characters.
    const received = Buffer.from(signature);
    return received.length === expected.length && crypto.timingSafeEqual(received, expected);
  });
}
Python
import base64, hashlib, hmac, time

def verify_wisply_webhook(secret: str, headers: dict, raw_body: bytes, tolerance_seconds: int = 300) -> bool:
    msg_id = headers.get("webhook-id")
    timestamp = headers.get("webhook-timestamp")
    signatures = headers.get("webhook-signature")
    if not (msg_id and timestamp and signatures):
        return False
    try:
        sent_at = int(timestamp)
    except ValueError:
        return False
    if abs(time.time() - sent_at) > tolerance_seconds:
        return False

    key = base64.b64decode(secret.removeprefix("whsec_"))
    signed = f"{msg_id}.{timestamp}.".encode() + raw_body
    expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest())

    for entry in signatures.split(" "):
        version, _, signature = entry.partition(",")
        # Compare bytes: compare_digest raises on non-ASCII str.
        if version == "v1" and hmac.compare_digest(signature.encode(), expected):
            return True
    return False

With the timestamp check turned off (the vector is from 2021), both snippets accept the published Standard Webhooks test vector:

Standard Webhooks test vector
InputValue
Secretwhsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw
webhook-idmsg_p5jXN8AQM9LWM0D4loKWxJek
webhook-timestamp1614265330
Body{"test": 2432232314}
webhook-signaturev1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=
Zapier and Make catch hooks can't check the signature without a code step. For them the URL itself is the secret: don't share it, and replace the hook if it leaks.

Connect a service

In each service, start with a webhook trigger, send a test event from Wisply so the service learns the fields, then map them into your apps.

Zapier

  1. Create a Zap with the trigger Webhooks by Zapier → Catch Hook and copy its URL (https://hooks.zapier.com/hooks/catch/…).
  2. Paste it into Wisply and tap Send test event; in Zapier, click Test trigger to load the sample memo.
  3. Add actions such as Linear → Create Issue or Google Docs → Create Document from Text, map data.title, data.summary, data.markdown or data.action_items, publish the Zap, and turn on webhooks in Wisply.

Make

  1. Create a scenario that starts with Webhooks → Custom webhook, add a webhook and copy its address (https://hook.<region>.make.com/…).
  2. Paste it into Wisply and tap Send test event while the module is listening, so Make learns the data structure.
  3. Add modules (Jira, Asana, Google Docs and others), map the fields, and turn the scenario and Wisply webhooks on. Use an Iterator on data.action_items to create one task per item.

n8n

  1. Add a Webhook node with HTTP method POST and copy its Production URL. A self-hosted n8n instance must be reachable over HTTPS.
  2. Paste it into Wisply. To capture the sample, switch the node to its Test URL, click Listen for test event, and tap Send test event; switch back to the production URL afterwards.
  3. Optionally add a Code node that checks webhook-signature (see Verify the signature), then connect the nodes for your apps and activate the workflow.

What's sent and what isn't

  • Sent: the memo id, title, date, duration, language, speaker names, AI summary, action items, open questions, a Markdown version, the Wisply version and, while Include full transcript is on, the full transcript.
  • Never sent: audio, and your signing secret, which signs each request but never leaves your device.
  • Sent directly from your device to the URL you entered. No Wisply server is in between.
  • What happens to a memo after delivery is up to the receiver and its own terms.

Read the privacy policy section.

Turn it off

Turn off Send finished recordings to a webhook (Send finished memos on iPhone and iPad), or clear the URL. Wisply stops sending at once and keeps nothing for the receiver; a request already in flight may still arrive. Memos already delivered stay wherever the receiver stored them, so delete them there.

Versioning

Every event carries api_version. The current version, and the first, is 2026-10-01.

It changes only when a field is removed or changes meaning. New fields can be added at any time, so ignore fields you don't recognize.