Manual · Connect

Webhooks

Send each note to another service when it is ready: your own second brain, an automation in n8n, Zapier or Make, or a chat channel in Discord, Slack or Telegram.

Last updated for A9 Voice 0.6.0

A webhook is a web address that another service gives you. When a note is ready, A9 Voice posts it to that address, and the service does whatever you set it up to do: file the note, make tasks from the action items, or post the summary in a channel.

You choose what is sent and which notes are sent. The service on the other end gets what you tick and nothing else. Audio, image files and voiceprints are never sent. Whoever can read that service can read what you send to it, so add only places you trust.

Set up a webhook#

  1. Open Settings › Notes › Integrations and click + Add webhook.

    The Integrations page with the + Add webhook button marked 1.

  2. Choose the Type. Under the menu, a line tells you where to get the address for that type.

    • Generic JSON, n8n, Zapier and Make get a JSON payload (see The JSON payload).
    • Discord, Slack and Telegram get the summary as text, trimmed to fit that service.
  3. Give it a Name (optional; the type's name is used if you leave it empty) and paste the URL. The URL is hidden as you type.

    The Add a webhook form with Type n8n, a Name, a hidden URL, the What to send ticks, When, Only, Signing secret and Header, filled in.

  4. For the JSON types, tick What to send (see What to send), then choose When, and optionally limit which notes with Only.

  5. Click Send a test to check it (see Send a test), then Save.

Saving needs a URL. If the address is wrong for the type you picked, the form says what it expects, for example "Paste the Discord webhook URL (https://discord.com/api/webhooks/…)" or "Use an https:// URL".

What each type needs#

TypeWhere the address comes fromWhat it receives
Generic JSONAny receiver that accepts a web requestThe JSON payload
n8nA Webhook trigger node (POST); paste its production URLThe JSON payload
Zapier"Webhooks by Zapier", then Catch Hook; paste its URLThe JSON payload
MakeA Custom webhook module; paste its addressThe JSON payload
DiscordServer settings › Integrations › Webhooks › New webhook › Copy URLThe summary as text
SlackAn incoming webhook for a channelThe summary as text
Telegramhttps://api.telegram.org/bot<token>/sendMessage?chat_id=<chat id>The summary as text

Discord, Slack and Telegram addresses are checked: Discord must be a discord.com webhook address, Slack must be on hooks.slack.com, and Telegram must be the sendMessage address with a chat_id.

A chat webhook sends the note's title, when it happened and the summary. A note without a summary has nothing to send, and the webhook says "This note has no summary yet".

The Add a webhook form with Type Discord: no What to send list, only When, Only, and a hidden URL.
Discord, Slack and Telegram send the summary only, so there is no What to send list.

What to send#

For the JSON types, tick any of these. Fields you do not tick are left out of the payload.

  • Summary
  • Action items (structured)
  • Decisions
  • Transcript: the whole conversation. This is off unless you turn it on, and the form warns you: "This sends the full conversation to this address."
  • Typed notes and markers
  • Image captions (never the image files)

When#

  • When a note finishes sends note.completed once, when the note is ready.
  • When a note finishes, and again when it changes (JSON types only) also sends note.updated after a rename, a speaker named, a summary redone or an edit, 60 seconds after the last change. Use it so a receiver stays in step with the note.
  • Manual only sends nothing by itself. See Send by hand.

Only some notes#

In the Only row, type tags (for example #customer) to send only notes that have at least one of them. Tick one or more templates to send only notes made with them. Leave both empty and every note is sent. This works for every type.

Security#

  • HTTPS only. An address must start with https://. For a receiver running on your own PC, http://localhost and http://127.0.0.1 are allowed too.
  • Signing secret (JSON types, optional). Paste any secret you make up, and each request carries a signature the receiver can check to be sure it came from A9 Voice. See Check the signature.
  • Header (JSON types, optional). A header name and a value, for example Authorization and Bearer .... The name can hold only letters, digits and dashes.
  • Kept in Windows Credential Manager. The URL, the signing secret and the header value are stored there, like your API keys. They are not in A9 Voice's settings file or its logs. When you edit a webhook, the fields show "Saved" and you paste a new value only to replace it.

The same form with a tag, a template ticked, Transcript ticked with its warning, a hidden signing secret, and a header name with a hidden value.

Retries and offline#

A send that fails is tried again after 10 seconds, after 1 minute and after 5 minutes. Then it gives up and the log says so. A send made while your PC is offline waits, and is tried again every minute, for up to 24 hours. Each attempt waits up to 20 seconds for an answer. Any answer in the 200s counts as delivered.

Send a test#

Send a test posts a made-up note ("Platform stand-up (test)", with Sam Example and Jane Doe) to the address and shows what came back. For the JSON types it is a real payload with the parts you ticked, signed and with your header. For chat types it is a short message: "A9 Voice test: this webhook works. Note summaries will arrive here."

On success the form says "Delivered (200)" and the start of the reply. On failure it says why: "Refused (401): check the URL, secret or header" (also for 403), "Not found (404): was the webhook deleted?", "Rate limited (429)", "No answer in 20 s" or "Couldn't reach it (offline?)".

The form after Send a test: Delivered (200), reply: {"ok":true}.

You can also click Test on a saved webhook in the list.

Your webhooks#

The list shows each webhook with its type, its name, when it sends, and its last result, such as "When a note finishes · Last: Thu 3:09 AM · delivered". A problem shows in amber, and a number says how many sends are waiting to be retried.

The list of three webhooks: a Discord one that delivered, an n8n one with Error 500, trying again in 1 min and 1 waiting, and a Zapier one set to Manual only.

Each row has Test, Log, Edit and Delete... (click it again to confirm). Deleting a webhook drops any sends still waiting for it.

The delivery log#

Log opens the list of sends for that webhook: the time, the event (note.completed or note.updated), the note's title, the HTTP status the receiver answered with, the size, and Resend. Resend sends that note again, the same way as any other send, with retries.

The delivery log for the n8n webhook: four lines with event, note title, status 500 or 200, size and Resend.

Send by hand#

On a note's page, each webhook has a Send to ... button in the right column with its name, and a line under it that says what happened last, for example "Sent automatically at 3:58 AM · delivered". Press it to send that note now as note.completed. A hand send is tried once, and the result is shown right there; use the log's Resend if you want retries.

The note page's webhook section: the name #MEETINGS, the button Send to #meetings and the line Sent automatically at 3:58 AM, delivered.

The JSON payload#

This is what Generic JSON, n8n, Zapier and Make receive. The data in this example is made up.

{
  "schema_version": 1,
  "event": "note.completed",
  "sent_at": "2026-10-05T17:41:00Z",
  "app": { "name": "A9 Voice", "version": "0.6.0" },
  "note": {
    "id": "8f0c2d1e-5b7a-4c3e-9d21-7a4e5f6b8c90",
    "title": "Platform stand-up",
    "started_at": "2026-10-05T16:00:00Z",
    "duration_sec": 1722,
    "source": "manual",
    "template": "Meeting Notes",
    "tags": ["stand-up"],
    "participants": [{ "name": "Jane Doe", "is_me": false, "talk_sec": 410 }],
    "summary_md": "## Overview\n…",
    "action_items": [{ "task": "Send checklist", "owner": "Jane Doe", "due": "2026-10-07", "mine": false }],
    "decisions": ["Release cut Thursday 4 PM"],
    "notes_md": "- [12:03] **Decision:** Release cut Thursday",
    "images": [{ "at_sec": 723, "caption": "Approval routing" }],
    "transcript": { "segments": [{ "start": 0.0, "end": 4.2, "speaker": "Jane Doe", "text": "…" }] }
  }
}

The fields:

  • schema_version: always 1 for this layout.
  • event: note.completed or note.updated.
  • sent_at: when the payload was prepared, in UTC. A retry sends the same payload, with the same sent_at.
  • app: the app's name and version.
  • note.id: stays the same for the life of the note, in every event, even if the note is renamed. A receiver can use it to update the same item instead of making a new one.
  • note.title, started_at (UTC), duration_sec, template (the template's name) and tags are always there.
  • note.source: a short lowercase id of the meeting app A9 Voice detected, other for an app you added yourself, or manual when you started the recording yourself.
  • note.participants: each speaker who talked, with their name as shown in the note, is_me (this is you) and talk_sec, their talk time in seconds. Someone who never spoke is left out.
  • summary_md: the summary as Markdown (Summary).
  • action_items: a list of task, owner, due and mine (it is one of your own items). due is the due date as it was said in the meeting, such as "Tue", "next week" or a date, or an empty string (Action items (structured)).
  • decisions: the bullet points under the summary's Decisions heading (Decisions).
  • notes_md: what you typed or marked during the meeting, one line each, with its time and, for markers, Action:, Decision: or Callout: (Typed notes and markers).
  • images: for each image, at_sec (when in the meeting) and caption. Never the image file (Image captions).
  • transcript.segments: each start and end in seconds, the speaker name and the text (Transcript).
  • Fields you did not tick are left out.
  • A very large transcript is dropped. If the whole payload would be over 10 MB, it is sent without the transcript and with "transcript_omitted": "too_large" in note.

Headers#

Each request is a POST with these headers:

  • Content-Type: application/json
  • User-Agent: A9 Voice webhooks
  • X-A9-Event: the event, note.completed or note.updated.
  • X-A9-Timestamp: when this attempt was made, in Unix seconds.
  • X-A9-Signature: only if you set a signing secret. See below.
  • Your own header, if you set one.

Check the signature#

With a signing secret, X-A9-Signature is sha256= followed by the hexadecimal HMAC-SHA256 of the raw request body, using your secret as the key. Compute it over the exact bytes you received, before parsing the JSON, and compare it before trusting the body. The timestamp header is not part of the signature.

import hashlib, hmac
expected = "sha256=" + hmac.new(SECRET.encode(), raw_body, hashlib.sha256).hexdigest()
ok = hmac.compare_digest(expected, request.headers["X-A9-Signature"])

Examples#

n8n#

  1. In n8n, add a Webhook node. Set the HTTP method to POST and respond Immediately. Copy its Production URL.
  2. In A9 Voice, open Settings › Notes › Integrations, click + Add webhook, choose n8n, paste the URL and tick what to send. Click Send a test.
  3. In n8n, the test arrives as body.note. For example, add a Split Out node on body.note.action_items to turn each action item into its own task in your tracker, keyed on body.note.id so an update replaces the earlier version.

Zapier#

  1. In Zapier, start a Zap with "Webhooks by Zapier" and the Catch Hook trigger. Copy the address it gives you.
  2. In A9 Voice, add a webhook of type Zapier, paste the address and tick what to send. Click Send a test.
  3. Back in Zapier, test the trigger. The test note arrives with the fields from The JSON payload, such as title, summary_md and action_items, which you can use in the steps that follow. Use id to tell notes apart.

Make#

  1. In Make, add a Custom webhook module as the start of a scenario. Copy its address.
  2. In A9 Voice, add a webhook of type Make, paste the address and tick what to send. Click Send a test.
  3. In Make, the test note's fields are available to the modules that follow. Use note.id to tell notes apart.

How you use the fields next is up to the service. Their own help covers their screens; A9 Voice only sends the payload above.

If something goes wrong#

  • Test says Refused (401) or (403). The receiver did not accept the request. Check the address, the signing secret and the header value. If the service needs a token, put it in Header.
  • Test says Not found (404). The address is wrong, or the webhook was deleted at the other end.
  • Nothing arrives, and the log says "trying again". The receiver is answering with errors or is slow. A9 Voice tries again after 10 seconds, 1 minute and 5 minutes, then gives up. Fix the receiver and use Resend in the log.
  • The webhook never fires. Check its When: Manual only sends only when you press Send to .... Check Only too: a tag or template filter that no note matches sends nothing.
  • A chat webhook says This note has no summary yet. Chat types send the summary, so the note needs one first. Add a language model and make a summary (see Language models).
  • "Couldn't reach it (offline?)". Your PC could not reach the address. Offline sends wait up to 24 hours and go out when you are back online.

More in Troubleshooting.

Something missing or wrong? Report an issue, or use Copy diagnostics in Settings › General when asking for help.