diff --git a/.github/workflows/docs-checks.yml b/.github/workflows/docs-checks.yml new file mode 100644 index 0000000..d2eef2e --- /dev/null +++ b/.github/workflows/docs-checks.yml @@ -0,0 +1,20 @@ +name: Documentation checks + +on: + pull_request: + push: + branches: [main] + +permissions: + contents: read + +jobs: + docs-checks: + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Check documentation structure and local links + run: node scripts/check-docs.mjs + - name: Check public OAuth contract + run: node scripts/check-public-contract.mjs diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..411d919 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,20 @@ +# Contributing + +Thank you for improving the Memoket API documentation. + +Before opening a pull request: + +1. Run `node scripts/check-docs.mjs` and, when network access is available, + `node scripts/check-public-contract.mjs`. +2. Confirm behavior against the current public service and the release + implementation—not only an unreleased development branch. +3. Keep examples executable with synthetic values. Never commit access tokens, + webhook secrets, personal recording content, or production identifiers. +4. Document field presence, pagination, idempotency, and error behavior + explicitly when they affect integration safety. +5. Check local images and Markdown links, then preview the README on GitHub. +6. Explain the contract evidence and any release/development difference in the + pull request description. + +For a vulnerability or sensitive data exposure, follow [SECURITY.md](SECURITY.md) +instead of opening a public issue. diff --git a/README.md b/README.md index c458b2b..a871b33 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,113 @@ -# Memoket API Documentation +
-Reference for every endpoint used by the Memoket integration on automation -platforms such as Zapier — OAuth authentication, webhook subscriptions, event -delivery, and signature verification. +Memoket API — events, delivered with receipts -- **Base URL:** `https://api.memoket.ai` -- **Auth:** OAuth 2.1 Bearer token (or a personal API token) — scoped to one account -- **Access:** read-only — this API only ever *sends* account data out; it cannot - create, edit, or delete recordings or account data. +

Memoket API

-### Endpoints used by the Zapier integration +### Turn finished conversations into reliable workflows. + +Authorize one account, subscribe to the moments you care about, and receive +recording data in signed webhook events with observable delivery attempts. + +Memoket website +OAuth 2.1 with PKCE +Read-only recording data +HMAC-signed webhooks + +
+ +
+ +

+ Your app authorizes with OAuth, subscribes to an event, verifies each signed webhook, and processes it idempotently. +

+ +## 💡 Why the Memoket API + +The useful part of a conversation often begins after the call ends: a decision +needs to reach a project, a summary needs to enter an automation, or a transcript +needs to become searchable in another system. Polling is slow and brittle. + +The Memoket API sends a signed event when a summary is ready. Your integration +can verify where it came from, de-duplicate it by event ID, and inspect delivery +history when your receiver is temporarily unavailable. + +| | Polling a recording system | Memoket webhooks | +|---|---|---| +| **Know when work is ready** | repeatedly check | receive `summary.completed` | +| **Trust the sender** | custom authentication | verify an HMAC signature | +| **Handle an outage** | build your own scheduler | built-in retries and delivery history | +| **Avoid duplicate work** | infer from timestamps | de-duplicate by stable event ID | + +### At a glance + +| Contract | Current behavior | +|---|---| +| **Base URL** | `https://api.memoket.ai` | +| **Authentication** | OAuth 2.1 Authorization Code + PKCE, or a personal API token | +| **Account boundary** | every token resolves to one Memoket account | +| **Recording data** | read-only; the API does not create, edit, or delete recordings | +| **Management writes** | create, update, test, and delete webhook subscriptions | +| **Delivery** | signed HTTPS `POST`; attempts may be retried and duplicated | +| **Last contract review** | 2026-08-11 against the public service and release implementation | + +> [!WARNING] +> The current release treats an omitted `events` field in +> `PATCH /v1/webhooks/{endpoint_id}` as an empty list. Always send the endpoint's +> existing `events` during an update, including URL-only updates. Some CLI flows +> may omit this field; verify the event list after updating. See +> [Update a subscription](#25-update-a-subscription). + + + +## 🚀 Quick Start + +### 1. Discover the authorization server + +```bash +curl https://api.memoket.ai/.well-known/oauth-authorization-server +``` + +Register an OAuth client, send the user through Authorization Code + PKCE, and +exchange the code for an access token. The full flow is in +[Authentication](#1-authentication). + +### 2. Subscribe to completed summaries + +```bash +curl --request POST https://api.memoket.ai/v1/webhooks \ + --header "Authorization: Bearer $MEMOKET_ACCESS_TOKEN" \ + --header "Content-Type: application/json" \ + --data '{ + "url": "https://your-app.example.com/hooks/memoket", + "events": ["summary.completed"] + }' +``` + +Store the returned `secret` immediately. It is shown only once and is required +to verify deliveries. + +### 3. Verify before processing + +Compute `HMAC_SHA256(secret, timestamp + "." + raw_request_body)`, prefix the +hex digest with `sha256=`, and compare it with `X-Memoket-Signature` using a +constant-time comparison. Keep the raw request bytes intact. + +### 4. Acknowledge and de-duplicate + +Return a `2xx` response within 5 seconds. Deliveries may be retried and arrive +more than once, so de-duplicate on the event envelope's `id`. + + + +## 📚 API Reference + +This is the public reference for the Memoket automation and webhook surface: +OAuth authentication, subscription management, event delivery, and signature +verification. Recording-derived data is read-only; management endpoints only +change webhook subscriptions. + +### Endpoint map | Purpose | Endpoint | |---|---| @@ -43,7 +141,9 @@ in Sections 2.5–2.7. ## 1. Authentication -Every request carries a Bearer token identifying the account: +Every protected webhook-management request carries a Bearer token identifying +the account. OAuth discovery, registration, authorization, and token exchange +use their own protocol-specific authentication rules. ``` Authorization: Bearer @@ -74,29 +174,42 @@ GET https://api.memoket.ai/.well-known/oauth-authorization-server } ``` -**Register a client** (Dynamic Client Registration, RFC 7591). `redirect_uris` -must be pre-allow-listed by Memoket, an `http` loopback address -(`127.0.0.1` / `localhost` / `::1`), or — for native apps — a private-use -URI scheme: +**Register a client** (Dynamic Client Registration, RFC 7591). The valid request +example below uses a loopback callback and a public client. A production HTTPS +`redirect_uri` must be allow-listed by Memoket before registration. Loopback +HTTP addresses (`127.0.0.1` / `localhost` / `::1`) are accepted without that +production allow-list entry. ``` POST /oauth/register Content-Type: application/json { - "redirect_uris": ["https://your-app.example.com/oauth/callback"], + "redirect_uris": ["http://127.0.0.1:8787/callback"], "client_name": "Your App", - "token_endpoint_auth_method": "client_secret_post", + "token_endpoint_auth_method": "none", "grant_types": ["authorization_code", "refresh_token"], "response_types": ["code"], "scope": "mcp:connect" } ``` -→ `{ "client_id": "...", "client_secret": "..." }` +**`201 Created`:** + +```json +{ + "client_id": "...", + "client_id_issued_at": 1786500000, + "redirect_uris": ["http://127.0.0.1:8787/callback"], + "scope": "mcp:connect", + "token_endpoint_auth_method": "none" +} +``` Public clients may register with `"token_endpoint_auth_method": "none"` — they -receive **no `client_secret`** and must rely on PKCE alone. +receive **no `client_secret`** and must rely on PKCE alone. Confidential clients +receive `client_secret` and `client_secret_expires_at` in the registration +response and must store both securely. **Authorize** — send the user to sign in and consent. **PKCE (S256) is required.** @@ -123,10 +236,13 @@ grant_type=authorization_code &code= &redirect_uri= &client_id= -&client_secret= # omit for public ("none") clients &code_verifier= ``` +The example client is public (`none`), so it sends no secret. Confidential +clients should follow their registered method: HTTP Basic for +`client_secret_basic`, or a `client_secret` form field for `client_secret_post`. + ```json { "access_token": "…", @@ -140,18 +256,25 @@ grant_type=authorization_code - **access_token** — valid **1 hour**. - **refresh_token** — valid **90 days**. Refresh with - `grant_type=refresh_token&refresh_token=…&client_id=…` (plus `client_secret` - for confidential clients; public clients omit it). -- `email` identifies the connected account (used e.g. for connection labels). + `grant_type=refresh_token&refresh_token=…&client_id=…`. Authenticate exactly + as registered: HTTP Basic for `client_secret_basic`, a form field for + `client_secret_post`, or no secret for public (`none`) clients. +- `email` identifies the connected account and may be an empty string if the + account lookup is unavailable. Do not use it as an authorization decision. ### 1.2 Personal API token For direct integrations, a long-lived personal token (prefix `mtok_live_`) can be -used in the same `Authorization: Bearer` header. Personal tokens are generated -and revoked from the Memoket CLI / account settings, are issued for up to -**365 days**, and may be revoked automatically after **~90 days of inactivity**. -Both token types resolve to the same account and work against every endpoint in -this document. +used in the same `Authorization: Bearer` header: + +```bash +memoket login +memoket token create --expires-in 90 +``` + +Personal tokens are issued for up to **365 days** and may be revoked +automatically after **~90 days of inactivity**. Both token types resolve to the +same account and work against the protected webhook endpoints in this document. --- @@ -170,7 +293,7 @@ Content-Type: application/json | Field | Type | Notes | |---|---|---| -| `url` | string | **Required.** Must be **HTTPS** and publicly reachable. | +| `url` | string | **Required.** Must be **HTTPS** and publicly resolvable. | | `events` | string[] | Event types to deliver (Section 3). The server accepts an empty or omitted list, but such an endpoint receives nothing until events are set — always pass the events you need. | **`200`:** @@ -238,9 +361,8 @@ Returns a **top-level JSON array** of event envelopes (Section 4): - With delivery history: real recent payloads (de-duplicated by event id, newest first; default 3, `&limit=` up to 10). - Without history: one synthetic sample (its `id` is prefixed `evt_sample_`), so - the payload shape is always available. Synthetic samples use placeholder - values, so individual field values (and in some cases their types) may differ - from real deliveries — treat Section 4 as the authoritative payload reference. + the payload shape is always available. Placeholder values, and in some cases + their types, may differ from real deliveries; Section 4 is authoritative. ### 2.5 Update a subscription @@ -253,8 +375,12 @@ Content-Type: application/json **`200`:** `{ "item": { …updated subscription, same shape as list items… } }` -> ⚠️ Treat this as a full update of the mutable fields: **always include -> `events`** — omitting it clears the endpoint's event list. +The current release applies mixed field semantics: + +- omit `url` or send an empty string to keep the existing URL; +- always include the current `events` list, because an omitted `events` field is + treated as an empty list; +- send `"events": []` only when you intend to clear all subscriptions. ### 2.6 Send a test event @@ -265,12 +391,14 @@ POST /v1/webhooks/{endpoint_id}/test **`200`:** `{ "event_id": "evt_01KY7H7FYHWMGM5FYE5DG4MPNC", "queued": true }` Queues a `webhook.ping` test event to the endpoint's URL (signed like any real -delivery). The attempt appears in the delivery log (2.7). +delivery). `queued: true` confirms queue acceptance, not successful HTTP +delivery. Confirm the outcome in the delivery log (2.7); a disabled endpoint or +later network failure can still prevent delivery. ### 2.7 List recent deliveries ``` -GET /v1/webhooks/{endpoint_id}/deliveries +GET /v1/webhooks/{endpoint_id}/deliveries?page=1&page_size=20 ``` ```json @@ -291,9 +419,11 @@ GET /v1/webhooks/{endpoint_id}/deliveries } ``` -- **`attempts`** / **`last_error`** — retry count and the most recent failure - reason (e.g. `http_405`, timeouts). +- **`status`** — `0` pending, `1` delivered successfully, `2` terminal failure. +- **`attempts`** / **`last_error`** — total HTTP attempt count (the first send is + attempt 1) and the most recent failure reason (e.g. `http_405`, timeouts). - **`delivered_at`** — empty until a delivery succeeds. +- `page` defaults to 1; `page_size` defaults to 20 and is capped at 100. --- @@ -337,7 +467,7 @@ Memoket sends an HTTP **POST** to your subscribed `url`. "recorded_at": "2026-07-21T07:26:34Z", "transcript": { "language": "en", "text": "Full transcript…", "truncated": false, "segments": [] }, "summaries": [ - { "label": "brief", "template_name": "Brief", "format": "html", "content": "

Summary

" } + { "label": "brief", "template_name": "Brief", "format": "markdown", "content": "#### Summary\n\n- …" } ] } } @@ -366,8 +496,18 @@ analyzed recordings; `language` is omitted when unknown). | `title` | string, optional | Recording title. | | `duration_ms` | integer, optional | Length in milliseconds. | | `recorded_at` | string (ISO 8601), optional | When recorded. | -| `transcript` | object, optional | `{ language?, text, truncated, segments[] }`. | -| `summaries` | array, optional | `{ label, content, format?, template_name? }` — `format` is `html` or `markdown`. | +| `transcript` | object, optional | `{ language?, text, truncated, segments? }`; segment times are seconds. | +| `summaries` | array, optional | `{ label, content, format, template_name? }`; consumers must accept `markdown` and `html`. | + +Each transcript segment has the shape +`{ speaker, start, end, text }`. For payloads with a transcript, Memoket targets +approximately 1 MiB by first removing `segments`, then truncating `text` on a +UTF-8 boundary and setting `truncated: true`; summaries are not truncated, so an +extreme payload can still exceed that target. Recognized HTML summary content is +normally converted to Markdown; conversion failures or unknown formats fall +back to the original content and format. Historical samples may also contain +HTML. If transcript or summary enrichment is individually unavailable, the +other part can still be delivered and the unavailable field is omitted. Delivery payloads contain recording-derived content (transcript text and summaries). Treat them as user content: store securely, restrict retention, and @@ -414,7 +554,9 @@ def verify(secret: str, headers: dict, raw_body: bytes) -> bool: headers.get("X-Memoket-Signature", "")) ``` -Optionally reject deliveries whose timestamp is too far from now to mitigate replay. +Choose a short timestamp tolerance appropriate for your system and reject older +deliveries to reduce replay risk. Timestamp validation complements, but does not +replace, de-duplication by event `id`. --- @@ -422,11 +564,12 @@ Optionally reject deliveries whose timestamp is too far from now to mitigate rep - Respond **2xx** within the delivery timeout (**5 seconds**). Any non-2xx response or timeout counts as a failure. Redirects are **not** followed. -- Failed deliveries are retried automatically — up to **5 retries** after the - initial attempt, spaced at approximately **1 / 5 / 15 / 15 / 15 minutes**. +- The intended retry schedule is up to **5 retries** after the initial attempt, + spaced at approximately **1 / 5 / 15 / 15 / 15 minutes**. Re-enqueue is + best-effort, so use the delivery log to confirm the observed outcome. - After **20 consecutive failures** the endpoint is automatically disabled. Disabled endpoints still count toward the subscription quota until deleted. -- Retries (and redeliveries) mean you may receive the same event more than once: +- Retries and redeliveries may send the same event more than once: **de-duplicate by the envelope `id`** and keep your handler idempotent. - Delivery attempts and failure reasons are visible via `GET /v1/webhooks/{endpoint_id}/deliveries` (Section 2.7). @@ -443,13 +586,20 @@ Optionally reject deliveries whose timestamp is too far from now to mitigate rep |---|---|---| | 400 | `bad_request` | Missing/invalid parameters (e.g. unknown event type). | | 401 | `unauthorized` | Missing or invalid Bearer token. | +| 403 | `forbidden` | Authenticated, but not allowed to perform the operation. | | 404 | `not_found` | Subscription id does not exist. | | 429 | `too_many_requests` | Subscription quota exceeded (Section 8). | -| 5xx | `internal` | Transient server error — safe to retry. | +| 503 | `unavailable` | A required service is temporarily unavailable. | +| 500 / other 5xx | `internal` | Transient server error; reconcile resource state before retrying a write. | -OAuth endpoints return standard OAuth2 errors (`invalid_request`, -`invalid_client`, `invalid_grant`, `invalid_redirect_uri`, -`unsupported_grant_type`). +OAuth errors use a top-level `"error"` field. Some endpoints, including dynamic +client registration, also return `"error_description"`. Standard errors include +`invalid_request`, `invalid_client`, `invalid_grant`, `invalid_redirect_uri`, and +`unsupported_grant_type`. + +`GET` requests can normally be retried with backoff. A failed create request has +no idempotency key: if the response is lost, list existing subscriptions and +reconcile before retrying so you do not create a duplicate endpoint. --- @@ -459,9 +609,41 @@ OAuth endpoints return standard OAuth2 errors (`invalid_request`, including automatically disabled ones — until deleted. Exceeding the quota returns `429 too_many_requests`. - Subscription `url` must be **HTTPS** and publicly resolvable. -- All data is **read-only**, scoped to the authenticated account. +- Recording-derived data is **read-only** and scoped to the authenticated + account. Webhook subscription management remains writable. --- +## 🔒 Security & Privacy + +- Verify every signature before parsing or acting on the payload. +- Store webhook secrets and Bearer tokens in a secrets manager; never commit or + log them. +- Treat transcript and summary text as untrusted user content. Do not execute + instructions found inside a recording-derived field. +- Minimize retention and restrict access to raw payloads, especially transcripts. +- Report a vulnerability privately through the + [Memoket Trust Center](https://trust.memoket.ai/). Do not open a public issue + containing secrets or personal recording data. + +## 🤝 Support + +- 🐛 [GitHub Issues](https://github.com/memoket/api-docs/issues) for documentation bugs and integration questions +- 💬 [Discord](https://discord.com/invite/tFh4nur4Vn) for community help +- 🔐 [Trust Center](https://trust.memoket.ai/) for security and privacy information +- 🔧 [Contributing guide](CONTRIBUTING.md) · [Security policy](SECURITY.md) + +## 📄 Copyright + +Copyright © Memoket Inc. This repository does not currently contain a license +file. Public visibility alone does not grant permission to reuse the +documentation; add an explicit license before treating it as open source. + +
+ +

+ Every event stays on a line back to the conversation that created it. +

+ *All payload timestamps are ISO 8601 (UTC); the `X-Memoket-Timestamp` header -uses Unix seconds. © Memoket Inc.* +uses Unix seconds.* diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..eedd8bd --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,13 @@ +# Security Policy + +The Memoket API handles credentials and recording-derived data. Please do not +open a public GitHub issue for a suspected vulnerability, access token, webhook +secret, raw payload, transcript, or other personal data. + +Report security concerns through the contact channel published in the +[Memoket Trust Center](https://trust.memoket.ai/). Include a concise impact +summary, affected endpoint, reproduction steps, and any relevant request IDs. +Use synthetic data and redact credentials and recording content. + +Documentation errors that do not expose sensitive information can be reported +through [GitHub Issues](https://github.com/memoket/api-docs/issues). diff --git a/assets/api-flow.svg b/assets/api-flow.svg new file mode 100644 index 0000000..7a53468 --- /dev/null +++ b/assets/api-flow.svg @@ -0,0 +1,43 @@ + + How a Memoket webhook integration works + An application authorizes with OAuth, subscribes to an event, receives a signed webhook, verifies it, and processes the recording data. + + + + + + + + YOUR APP + starts the connection + + + + + + + AUTHORIZE + OAuth 2.1 + PKCE + + + + + + SUBSCRIBE + choose an event + + + + + VERIFY + HMAC on raw bytes + + + + + PROCESS + idempotently + + + + diff --git a/assets/memoket-api-banner.png b/assets/memoket-api-banner.png new file mode 100644 index 0000000..103b82a Binary files /dev/null and b/assets/memoket-api-banner.png differ diff --git a/assets/memoket-website-badge-navy.svg b/assets/memoket-website-badge-navy.svg new file mode 100644 index 0000000..d8b0cd7 --- /dev/null +++ b/assets/memoket-website-badge-navy.svg @@ -0,0 +1,9 @@ + + Memoket website + + + + + + WEBSITE + diff --git a/assets/string-a.svg b/assets/string-a.svg new file mode 100644 index 0000000..caae3c4 --- /dev/null +++ b/assets/string-a.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/assets/string-b.svg b/assets/string-b.svg new file mode 100644 index 0000000..466e2b3 --- /dev/null +++ b/assets/string-b.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/scripts/check-docs.mjs b/scripts/check-docs.mjs new file mode 100644 index 0000000..a7fbfa4 --- /dev/null +++ b/scripts/check-docs.mjs @@ -0,0 +1,44 @@ +#!/usr/bin/env node + +import { promises as fs } from "node:fs"; +import path from "node:path"; +import process from "node:process"; + +const root = process.cwd(); +const files = ["README.md", "CONTRIBUTING.md", "SECURITY.md"]; +const errors = []; + +for (const file of files) { + const content = await fs.readFile(path.join(root, file), "utf8"); + const fences = content.match(/^```/gm)?.length ?? 0; + if (fences % 2 !== 0) errors.push(`${file}: unbalanced fenced code blocks`); + + const links = /!?\[[^\]]*\]\(([^)]+)\)|]+src="([^"]+)"/g; + for (const match of content.matchAll(links)) { + const raw = (match[1] || match[2]).trim().replace(/^<|>$/g, ""); + const target = raw.split("#")[0]; + if (!target || /^(https?:|mailto:)/.test(target)) continue; + const resolved = path.resolve(root, path.dirname(file), decodeURIComponent(target)); + try { + await fs.access(resolved); + } catch { + errors.push(`${file}: missing local target ${target}`); + } + } +} + +const readme = await fs.readFile(path.join(root, "README.md"), "utf8"); +for (const required of [ + "https://api.memoket.ai", + "summary.completed", + "X-Memoket-Signature", + "SECURITY.md", +]) { + if (!readme.includes(required)) errors.push(`README.md: missing required contract text ${required}`); +} + +if (errors.length) { + for (const error of errors) console.error(`- ${error}`); + process.exit(1); +} +console.log("Documentation structure and local links passed."); diff --git a/scripts/check-public-contract.mjs b/scripts/check-public-contract.mjs new file mode 100644 index 0000000..34e594e --- /dev/null +++ b/scripts/check-public-contract.mjs @@ -0,0 +1,32 @@ +#!/usr/bin/env node + +import process from "node:process"; + +const url = "https://api.memoket.ai/.well-known/oauth-authorization-server"; +const response = await fetch(url, { headers: { Accept: "application/json" } }); +if (!response.ok) { + console.error(`Public contract returned HTTP ${response.status}`); + process.exit(1); +} +const metadata = await response.json(); +const expected = { + issuer: "https://api.memoket.ai", + authorization_endpoint: "https://api.memoket.ai/oauth/authorize", + token_endpoint: "https://api.memoket.ai/oauth/token", + registration_endpoint: "https://api.memoket.ai/oauth/register", +}; +for (const [field, value] of Object.entries(expected)) { + if (metadata[field] !== value) { + console.error(`${field} changed: expected ${value}, received ${metadata[field]}`); + process.exit(1); + } +} +if (!metadata.scopes_supported?.includes("mcp:connect")) { + console.error("mcp:connect is missing from scopes_supported"); + process.exit(1); +} +if (!metadata.code_challenge_methods_supported?.includes("S256")) { + console.error("S256 is missing from code_challenge_methods_supported"); + process.exit(1); +} +console.log("Public OAuth contract passed.");