Developer Docs
Quickstart
Create an API key, make your first request, and receive webhooks securely in four steps.
1. Create an API key
Create an integration in the account console and issue an API key. The full key is shown only once, so store it securely in a secret manager.
- Account โ API & Webhooks โ create an integration for your personal workspace or a teamspace you own.
- Pick scopes:
notes:read,transcripts:read,summaries:read,webhooks:manage. - The key looks like
alt_live_{key_id}.{secret}and is shown once. Store it in a secret manager.
Add the API key to your shell environment so the commands below run as written:
export ALT_API_KEY="alt_live_...paste-your-key-here..."2. Fetch existing notes
Pass the cursor returned by the API into the next request to continue through the entire note list. Then fetch the transcript and summary for each note.
curl 'https://public-api.altalt.io/v1/notes?limit=100' \
-H "Authorization: Bearer $ALT_API_KEY"
# Follow next_cursor until has_more is false
curl 'https://public-api.altalt.io/v1/notes?limit=100&cursor=NEXT_CURSOR' \
-H "Authorization: Bearer $ALT_API_KEY"
# Fetch content per note (scopes: transcripts:read / summaries:read)
curl 'https://public-api.altalt.io/v1/notes/NOTE_ID/transcript' \
-H "Authorization: Bearer $ALT_API_KEY"
curl 'https://public-api.altalt.io/v1/notes/NOTE_ID/summary' \
-H "Authorization: Bearer $ALT_API_KEY"After the first sync, you do not need to fetch every note again. Use ?updated_after=<last sync time> to request only notes changed since the last sync, or rely on webhooks.
3. Register a webhook endpoint
Register a public HTTPS endpoint so you can receive notifications about new and changed notes instead of repeatedly polling the API.
curl -X POST 'https://public-api.altalt.io/v1/webhook-endpoints' \
-H "Authorization: Bearer $ALT_API_KEY" \
-H 'Content-Type: application/json' \
-d '{
"url": "https://example.com/webhooks/alt",
"events": ["note.ended", "note.summary.generated", "note.updated", "note.deleted"]
}'The response includes the signing_secret (whsec_...) used to verify webhook signatures. The full value is shown once. The endpoint starts in pending_verification and becomes active after your receiver responds to the verification event with a 2xx. You can also register it without code in the console.
4. Verify webhook signatures
Verify the Standard Webhooks signature on every request to confirm it came from Alt. Use event_id to skip duplicates, respond first, and then fetch the latest content from the REST API.
Node.js
import { createHmac, timingSafeEqual } from "node:crypto";
import http from "node:http";
// whsec_... secret from endpoint creation (shown once). Keep it server-side.
const SECRET = process.env.ALT_WEBHOOK_SECRET;
const secretBytes = Buffer.from(SECRET.slice("whsec_".length), "base64url");
const TOLERANCE_SECONDS = 300;
function isValidSignature(headers, rawBody) {
const id = headers["webhook-id"];
const timestamp = headers["webhook-timestamp"];
const signatureHeader = headers["webhook-signature"];
if (!id || !timestamp || !signatureHeader) return false;
// Reject stale timestamps (replay protection)
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > TOLERANCE_SECONDS) return false;
const expected = createHmac("sha256", secretBytes)
.update(`${id}.${timestamp}.${rawBody}`)
.digest("base64");
// Header may contain multiple space-delimited signatures: "v1,abc v1,def"
return String(signatureHeader)
.split(" ")
.some((part) => {
const [version, signature] = part.split(",");
if (version !== "v1" || !signature) return false;
const a = Buffer.from(signature);
const b = Buffer.from(expected);
return a.length === b.length && timingSafeEqual(a, b);
});
}
http
.createServer((req, res) => {
if (req.method !== "POST" || req.url !== "/webhooks/alt") {
res.writeHead(404).end();
return;
}
let rawBody = "";
req.on("data", (chunk) => (rawBody += chunk));
req.on("end", () => {
if (!isValidSignature(req.headers, rawBody)) {
res.writeHead(401).end();
return;
}
const event = JSON.parse(rawBody);
// 1. Dedupe on event.event_id (deliveries are at-least-once).
// 2. Enqueue for async processing, then ack fast.
// 3. Fetch the note from the REST API; apply only if revision is newer.
console.log(event.event_type, event.data.note_id, event.data.revision);
res.writeHead(204).end();
});
})
.listen(3000);Python
import base64, hashlib, hmac, json, os, time
from http.server import BaseHTTPRequestHandler, HTTPServer
# whsec_... secret from endpoint creation (shown once). Keep it server-side.
raw_secret = os.environ["ALT_WEBHOOK_SECRET"].removeprefix("whsec_")
SECRET = base64.urlsafe_b64decode(raw_secret + "=" * (-len(raw_secret) % 4))
TOLERANCE_SECONDS = 300
def is_valid_signature(headers, raw_body: bytes) -> bool:
msg_id = headers.get("webhook-id", "")
timestamp = headers.get("webhook-timestamp", "")
signature_header = headers.get("webhook-signature", "")
if not msg_id or not timestamp or not signature_header:
return False
# Reject stale timestamps (replay protection)
if abs(time.time() - float(timestamp)) > TOLERANCE_SECONDS:
return False
signed_content = f"{msg_id}.{timestamp}.".encode() + raw_body
digest = hmac.new(SECRET, signed_content, hashlib.sha256).digest()
expected = base64.b64encode(digest).decode()
# Header may contain multiple space-delimited signatures: "v1,abc v1,def"
for part in signature_header.split(" "):
version, _, signature = part.partition(",")
if version == "v1" and signature and hmac.compare_digest(signature, expected):
return True
return False
class Handler(BaseHTTPRequestHandler):
def do_POST(self):
if self.path != "/webhooks/alt":
self.send_response(404); self.end_headers(); return
raw_body = self.rfile.read(int(self.headers.get("Content-Length", 0)))
if not is_valid_signature(self.headers, raw_body):
self.send_response(401); self.end_headers(); return
event = json.loads(raw_body)
# 1. Dedupe on event["event_id"] (deliveries are at-least-once).
# 2. Enqueue for async processing, then ack fast.
# 3. Fetch the note from the REST API; apply only if revision is newer.
print(event["event_type"], event["data"]["note_id"], event["data"]["revision"])
self.send_response(204); self.end_headers()
HTTPServer(("", 3000), Handler).serve_forever()Signatures follow the Standard Webhooks specification, so you can use the official standardwebhooks libraries for npm and PyPI. See Webhooks for guidance on duplicates, delivery order, and missed changes.