Docs

Integrations and webhooks

Push finished work to any URL you own. Delivery happens on a background thread, so a note is pasted and saved whether or not your endpoint answers, and every attempt is reported in the Activity feed. This page has the configuration, the JSON payload as it arrives, the three ways to authenticate it, and a worked example of verifying an HMAC signature in Node.

Integrations and webhooks

Push finished work to another service. Add a destination to %USERPROFILE%\.deixis\config.toml:

[[integrations]]
name = "notion"                        # the handle used everywhere: refusals, Test, "Send to"
url = "https://example.com/deixis"     # http(s) only
secret = "paste-it-here"               # rewritten as dpapi:<ciphertext> on the next save
auth = "hmac"                          # hmac (default) | bearer | header
events = ["note", "capture", "translation"]   # omit for all three
trigger = "auto"                       # auto (on finish) | manual (the note's Send to button only)

Or use Settings → Integrations, which has a Test button per destination that posts a sample payload and shows you the status and response.

What arrives is one JSON POST:

{
  "v": 1,
  "event": "note",
  "id": "2026-08-26T09-15-00",
  "created_at": "2026-08-26T09:15:00Z",
  "duration_ms": 42000,
  "app": "",
  "title": "…",
  "text": "…",
  "markdown": "…",
  "references": [{ "kind": "element", "path": "C:\\Users\\you\\.deixis\\...png", "text": "Save", "t": 12.5,
                   "source": "chrome.exe", "name": "Save", "role": "button", "id": "save-btn",
                   "class": "btn btn-primary", "page": "https://example.com/settings",
                   "href": "", "subrole": "", "level": 0, "disabled": false,
                   "within": { "role": "form", "name": "Account", "id": "account-form" } }]
}

Every reference field is always present, and empty where it does not apply — an empty string, 0 for level, false for disabled, and for within an object of empty strings rather than null. source names the app a selection or element came from (an image's file name for a grab), and the rest describe a picked element: name is what it is called, role its control type and subrole what the page calls it where that says more (heading, navigation, region); id and class are what a search of the source finds; level is a heading's level, 1 to 9, and 0 for anything else; disabled says it was greyed out when it was picked; and within is the nearest enclosing element the page names or identifies — the section a heading heads, the navigation a link sits in. page, and a link's href, are the address without its query string, fragment, user:password@ or ;jsessionid=-style path parameters, which is where sign-in tokens live; a page opened from disk is only its file name, file:///…/index.html, never the folders above it. A pick of a page's blank area, a window or a title bar files no name, because each of those is named by its title.

Authentication, by mode:

  • hmac (recommended) — X-Deixis-Timestamp: <unix seconds> and X-Deixis-Signature: sha256=<hex>, the signature being HMAC-SHA256 of {timestamp}.{body} with your secret. The secret itself never travels, and the timestamp lets you reject replays.
  • bearer — Authorization: Bearer <secret>.
  • header — X-Deixis-Key: <secret>.

Verifying an HMAC delivery, in Node:

import { createHmac, timingSafeEqual } from "node:crypto";

const raw = await req.text();                       // the exact bytes, before JSON.parse
const ts = req.headers.get("x-deixis-timestamp");
const sent = req.headers.get("x-deixis-signature") ?? "";
const mine = "sha256=" + createHmac("sha256", SECRET).update(`${ts}.${raw}`).digest("hex");
const ok = sent.length === mine.length && timingSafeEqual(Buffer.from(sent), Buffer.from(mine));
if (!ok || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return new Response("no", { status: 401 });

Details that matter:

  • Delivery never blocks you. It happens on a background thread: three attempts with backoff, a 10-second ceiling each, and every outcome shown in the Activity feed. A finished note is pasted and saved whether or not your endpoint answers.
  • Retry by hand from the note's card in Captures — the same "Send to" button that sends manual destinations.
  • Secrets are encrypted at rest with Windows DPAPI, tied to your Windows account. A config.toml copied to another machine (or another user) keeps everything except the secret, which has to be re-pasted there — that is DPAPI working, not a bug.
  • Images travel as paths, not bytes: a receiver on this machine can read them, one elsewhere cannot.

Two keycaps, Ctrl and Win, held down and lit teal from underneath.

Hold a key. Speak. Keep working.

Deixis is free, needs no account, and runs on your own device.

v1.3.2 · 64‑bit Windows 10/11 on Intel or AMD · macOS 13 on Apple silicon · 51 MB · nothing else to install · Android on Google Play