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 +
-- **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.
+
+
+
&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 @@
+
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 @@
+
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.");