This is the full developer documentation for Reminix
# Introduction
> Reminix turns scripts and AI-written code into safe, shared capabilities your team can run.
AI makes code cheap to write. Reminix makes it usable by a team. A script, a workflow, a piece of code you or an agent wrote becomes a **capability**: something your team can run safely and share —
* **with a UI for people**, an **API and tool interface for software and agents**, and **scheduled or event-driven runs**;
* **governed**: who may run it, which secrets it may use, which runs need a person’s approval, every run logged, every change a new version.
Reminix is built for agents as well as people. Every operation is available through the [API](/docs/quickstart/), the [command line](/docs/cli/) and the [MCP server](/docs/mcp/), so an agent can publish and run capabilities for your team and ask before doing anything that needs a person ([Approvals](/docs/approvals/)).
Available now
Publish JavaScript and TypeScript capabilities from your repository and run them from the app, the API, the command line and agents — start with [Capabilities](/docs/capabilities/) — calling APIs with your secrets attached included — with team access, approvals, schedules and triggers.
# Use with AI agents
> Let an AI agent work with your workspace — through the MCP server, the command line, or the API — and teach it how with the agent skill.
AI agents can do in your workspace what you could do yourself — within the access you give them, as **you**, and recorded in the audit log. Three ways, from the least setup:
| Your agent | Use |
| ------------------------------------------ | ------------------------------------------------- |
| Claude, ChatGPT or another MCP-capable app | the [MCP server](/docs/mcp/) — nothing to install |
| A coding agent with a terminal | the [command line](/docs/cli/), with `--json` |
| Your own software | the [API](/docs/quickstart/) or [SDK](/docs/sdk/) |
## MCP server
[Section titled “MCP server”](#mcp-server)
Add `https://mcp.reminix.com/mcp` as a remote MCP server in your agent. You sign in once in the browser, choose **one workspace** and what the agent may do; its tools are exactly that. Details and safeguards: [MCP server](/docs/mcp/).
## Command line
[Section titled “Command line”](#command-line)
Coding agents (Claude Code, Cursor, Codex …) work well with the [command line](/docs/cli/):
```bash
npm install -g @reminix/cli
reminix login # you approve it in the browser
reminix workspace list --json
reminix workspace webhook-endpoints list --workspace acme --json
```
`--json` gives the agent the API’s exact JSON, errors as JSON on stderr, and a 0/1 exit code. In CI or a sandbox without a browser, set `REMINIX_TOKEN` to a [personal access token](/docs/authentication/) limited to what the agent needs.
## Teach your agent: the skill
[Section titled “Teach your agent: the skill”](#teach-your-agent-the-skill)
The agent skill is one file that teaches an agent everything above — how to sign in, choose the workspace, every command and tool, the error codes, and what to leave to a person: [`/docs/skill/SKILL.md`](https://reminix.com/docs/skill/SKILL.md). It is generated from the API, so it always matches.
For Claude Code, install it for every project:
```bash
mkdir -p ~/.claude/skills/reminix
curl -fsSL https://reminix.com/docs/skill/SKILL.md \
-o ~/.claude/skills/reminix/SKILL.md
```
(or into a project’s `.claude/skills/reminix/`). Other skills-aware agents take the same file in their own skills folder.
## Docs for agents
[Section titled “Docs for agents”](#docs-for-agents)
The documentation is available as plain text for agents: [`/docs/llms.txt`](/docs/llms.txt) (an index of every page) and [`/docs/llms-full.txt`](/docs/llms-full.txt) (everything in one file). The site’s [`/llms.txt`](https://reminix.com/llms.txt) indexes these, the API, the MCP server and the skill.
## What agents cannot do
[Section titled “What agents cannot do”](#what-agents-cannot-do)
Some operations stay with people even when an agent has full access — anything that sends your workspace’s data somewhere new or hands out access (for example, creating API keys or adding webhook endpoints). An agent asks instead, and you approve or deny — see [Approvals](/docs/approvals/); a secret it creates never reaches the agent.
# Approvals
> When an agent needs something it may not do alone — an API key, a webhook — it asks, and a person decides.
Some actions are never an agent’s to take alone, because they hand out access or send your data somewhere new: creating an API key, adding, removing or re-enabling a webhook endpoint. An agent that needs one **asks**, and a person who could do it themselves decides.
## How it works
[Section titled “How it works”](#how-it-works)
1. **The agent asks** — with the action, its details and a reason (“I’m connecting acme-web and need an API key that can read the workspace”). Everyone who could approve it gets a notification.
2. **A person decides** at the link — the notification, or the one the agent shows you. You see exactly what will happen and why. **Approve** or **Deny**.
3. **It happens as you** — with your permissions, recorded in the audit log as yours, noting the agent that asked.
Requests expire after 24 hours, and each can be decided only once.
## Secrets stay out of the agent’s hands
[Section titled “Secrets stay out of the agent’s hands”](#secrets-stay-out-of-the-agents-hands)
When the action creates a secret (an API key, a webhook signing secret), the agent never sees it:
* **Asked from the command line**: after you approve, the command line writes the secret straight into a file in your project — it is never printed, so an agent driving the terminal cannot read it:
```bash
reminix workspace approval-requests create --action workspace.apiKey.create \
--input '{"name":"acme-web","scopes":["workspace:read"]}' \
--reason "Connect acme-web" --json
reminix workspace approval-requests redeem --wait --write-env .env
# Done: … Wrote REMINIX_API_KEY to .env (the value is not shown).
```
* **Asked by an agent (MCP)**: the action runs when you approve, and the secret is shown to **you**, once, on the approval page.
## For developers
[Section titled “For developers”](#for-developers)
`GET /v1/workspace/approval-actions` lists what can be asked (each with its input schema and who approves it); `POST /v1/workspace/approval-requests` asks; `GET /v1/workspace/approval-requests/{id}` reports the decision. Asking needs the `workspace.approvals:write` scope and a person’s credential — a personal access token or an app a person connected, not a workspace API key.
# Authentication
> Authenticate public API requests with Workspace API keys.
The public API authenticates requests with **Workspace API keys** using the standard Bearer scheme:
```http
Authorization: Bearer YOUR_API_KEY
```
```bash
curl \
-H "Authorization: Bearer YOUR_API_KEY" \
https://api.reminix.com/v1/workspace
```
A successful response identifies the workspace the key belongs to:
```json
{ "id": "…", "name": "Example Workspace" }
```
## API keys belong to a workspace
[Section titled “API keys belong to a workspace”](#api-keys-belong-to-a-workspace)
Keys authenticate a **workspace**, not a person. A key keeps working even if the person who created it leaves the workspace, and it never carries a user session — it cannot be used to sign in.
## Creating a key
[Section titled “Creating a key”](#creating-a-key)
Workspace owners can create keys in the application under **Settings → API Keys**:
1. Choose a descriptive name (for example `production-backend`).
2. Choose what the key may do (see [Scopes](#scopes)) — **Read only** by default.
3. Copy the secret when it is shown — **it is displayed exactly once** and cannot be viewed again. Store it in your secret manager.
4. The key list shows only safe metadata (name, first characters, access, creation and last-used times).
If a secret is lost, create a replacement key and revoke the old one.
## Revoking and rotating
[Section titled “Revoking and rotating”](#revoking-and-rotating)
Owners can revoke a key at any time from **Settings → API Keys**. Revocation is immediate and permanent: the same secret will fail authentication on the next request.
To rotate a key without downtime:
1. Create a replacement key.
2. Update your integration to use it.
3. Revoke the old key.
## Scopes
[Section titled “Scopes”](#scopes)
Every key and token is granted **scopes** when it is created, and can do only what it was granted. A scope names a group of endpoints and whether it may read or change them — for example `workspace.audit:read` (read the audit trail), `workspace.members:read` (read members and pending invitations) or `workspace:read` (read the workspace profile). Each endpoint in the [API reference](https://api.reminix.com/v1/docs) states the scope it needs.
When you create a credential you choose one of:
* **Read only** (the default) — every read scope available at creation. A scope added to the API later is not granted automatically.
* **Full access** — everything, including scopes added later.
* **Custom** — exactly the scopes you tick.
Grant only what an integration needs. A request for an endpoint the credential was not granted is a `403` with code `forbidden`:
```json
{
"error": {
"code": "forbidden",
"message": "This credential is not granted the workspace.audit:read scope",
"requestId": "…"
}
}
```
To change what an integration may do, create a new credential with the access it needs and revoke the old one.
## Personal access tokens
[Section titled “Personal access tokens”](#personal-access-tokens)
A second credential type exists for acting as **yourself** rather than as a workspace — for scripts, CI jobs and the command line: personal access tokens (`pat_…`), created in the application under **Account → API tokens** — or by the command line itself: `login` opens your browser, you approve it, and the token for that computer is created and stored for you. A personal access token:
* acts as you, in the workspaces you choose at creation (all of yours, or specific ones), with the [scopes](#scopes) you grant it;
* is subject to your workspace role as well: `GET /v1/workspace/audit-events` is owner-only, like the audit page in the application (a workspace API key is the workspace and has no role);
* expires on the schedule you pick (30 days, 90 days, a year, or never);
* answers the identity check on `/v1/user/me`:
```bash
curl \
-H "Authorization: Bearer YOUR_PERSONAL_ACCESS_TOKEN" \
https://api.reminix.com/v1/user/me
```
```json
{
"id": "…",
"name": "Casey Customer",
"email": "casey@example.com",
"workspaces": [
{ "id": "…", "name": "Example", "slug": "example", "role": "owner" }
]
}
```
On every other endpoint a personal access token must say which workspace it acts in, with the `X-Workspace` header (the workspace slug); the call then runs with your membership in that workspace and the token’s scopes:
```bash
curl \
-H "Authorization: Bearer YOUR_PERSONAL_ACCESS_TOKEN" \
-H "X-Workspace: example" \
https://api.reminix.com/v1/workspace
```
A missing `X-Workspace` is a `400`; a workspace you are not a member of, or one the token was not scoped to, is a `403` — as is a scope the token was not granted. Workspace API keys ignore the header: a key is its workspace. A workspace API key is never accepted on `/v1/user/*`. Like API keys, tokens are shown once at creation, revocable from the same page, and stop working immediately if your account is suspended or the token expires.
## Apps and AI agents (OAuth)
[Section titled “Apps and AI agents (OAuth)”](#apps-and-ai-agents-oauth)
Apps — including AI agents’ tool connections — can act for a person without ever seeing a key: the product is an OAuth 2.1 authorization server. The app sends the person to sign in, they choose a workspace and approve what the app may do, and the app receives a short-lived access token for the API.
* Discovery: `https://api.reminix.com/.well-known/oauth-authorization-server/auth` lists every endpoint. Apps may register themselves (dynamic client registration); a registration grants nothing until a person approves.
* An app asks for the same [scopes](#scopes) (`workspace.audit:read`, …), plus `offline_access` for a refresh token. The person can narrow them.
* A token acts for one person in one workspace — no `X-Workspace` header — with that person’s role. Request it for the resource `https://api.reminix.com/v1`.
* The person can disconnect the app at any time under **Account → Connected apps**; its tokens stop working on the next request.
## Authentication failures
[Section titled “Authentication failures”](#authentication-failures)
Requests with a missing, malformed, invalid, or revoked credential receive a `401` with the standard [error envelope](/docs/errors/):
```json
{
"error": {
"code": "unauthorized",
"message": "Unauthorized",
"requestId": "…"
}
}
```
The response is deliberately identical across failure causes (no enumeration help). Browser session cookies are never accepted on the public API.
# Capabilities
> Turn a script your team (or an agent) wrote into a shared, versioned capability.
A **capability** is a script or small app your team can run safely — from the app, the API, or an AI agent. It has a name and an owner, the JSON Schema of its inputs (which becomes the form people fill in and the input agents send), and **versions**: every change is a new version, and runs use the one that is **published**.
## The folder: `reminix.json` + one file
[Section titled “The folder: reminix.json + one file”](#the-folder-reminixjson--one-file)
```json
{
"slug": "refund-customer",
"name": "Refund a customer",
"description": "Refunds an order and notifies the customer.",
"entry": "index.ts",
"inputSchema": {
"type": "object",
"required": ["orderId"],
"properties": { "orderId": { "type": "string" } }
}
}
```
```ts
// index.ts — the entry; it may import other files and npm packages
import { refund } from "./payments.ts";
export default async function run(inputs: { orderId: string }, ctx) {
ctx.log(`refunding ${inputs.orderId}`);
return { refunded: inputs.orderId };
}
```
The entry default-exports `run(inputs, ctx)` and returns JSON. JavaScript and TypeScript both work. When you publish, the entry and everything it imports — your other files and npm packages from the folder’s `node_modules` (run `npm install` first) — are bundled into one module, at most 5 MB. Node built-ins (`node:crypto`, `node:buffer` …) are available as imports.
Add `"tags": ["finance", "refunds"]` to `reminix.json` (up to 10, lowercase) and a `README.md` beside it — the capability’s page shows the README, and both help your team [find it](/docs/finding-capabilities/).
## Calling APIs
[Section titled “Calling APIs”](#calling-apis)
By default a capability has no network. To call an API, list its hosts, and the secrets to attach to requests to them:
```json
{
"hosts": ["api.stripe.com"],
"secrets": {
"STRIPE_KEY": {
"host": "api.stripe.com",
"header": "Authorization",
"value": "Bearer {secret}"
}
}
}
```
```ts
export default async function run(inputs, ctx) {
// No key in the code: Reminix adds the Authorization header on the way out.
const res = await fetch("https://api.stripe.com/v1/refunds", {
method: "POST",
body: new URLSearchParams({ payment_intent: inputs.paymentId }),
});
return await res.json();
}
```
* Requests go out only to `hosts`, over HTTPS; anything else fails with “Host not allowed”, and every request is in the run’s log.
* Each secret is attached only to its `host` — and only if the secret itself allows that host ([Secrets](/docs/secrets/)). The code never holds the value. (An API that echoes your credentials back in its response would reveal them to the code — don’t send secrets to one.)
* `value` defaults to `{secret}`; use it for the scheme, e.g. `"Bearer {secret}"`.
* To read a secret as a plain value (`ctx.env.NAME`), list it in `"env": ["NAME"]` — allowed only for a secret whose owner turned on “Let capabilities read the value itself”.
The capability page shows each version’s hosts and secrets, so whoever publishes sees what it can reach.
## Publishing
[Section titled “Publishing”](#publishing)
```bash
reminix capabilities publish ./refund-customer # uploads a draft version
reminix capabilities publish ./refund-customer --publish # …and publishes it
```
The first publish creates the capability. Drafts change nothing until a version is **published** — by the capability’s owner, or a workspace owner or admin. An agent’s publish becomes a request a person approves. Publishing an older version again rolls back. See [Governance](/docs/governance/) for who can use a capability, runs that need approval, and a second person for publishing.
Through the API, with a key or token holding `capabilities:write`: `POST /v1/capabilities` (create), `POST /v1/capabilities/{slug}/versions` (upload `{ entry, inputSchema, code }`), and `POST /v1/capabilities/{slug}/versions/{number}/publish`.
## Running it
[Section titled “Running it”](#running-it)
Once published, anyone in the workspace can run it:
* **In the app:** Capabilities → the capability. Its input schema becomes a form; **Run** shows the output and the log.
* **From the command line:** `reminix capabilities run refund-customer --input '{"orderId":"o_123"}'` prints the output (`--log` also prints the log).
* **From an agent:** through the [MCP server](/docs/mcp/), every published capability is a tool named `run_` (`run_refund_customer`) taking its inputs.
* **From the API:** `POST /v1/capabilities/{slug}/runs` — see [Runs](/docs/runs/).
# Command line
> Install the CLI, sign in, and call every API operation from a terminal or script.
The command line talks to the same public API as your code, as **you** (a personal access token), across every workspace you allow it.
## Install
[Section titled “Install”](#install)
```bash
npm install -g @reminix/cli
```
## Sign in
[Section titled “Sign in”](#sign-in)
```bash
reminix login
```
Your browser opens: sign in, choose which workspaces the command line may use and what it may do, and click **Allow**. A personal access token for this computer is created (valid for 90 days, listed under **Account → API tokens**) and stored in `~/.config/reminix/`.
On a server or in CI there is no browser — pass a token instead, or set it in the environment (nothing is written to disk):
```bash
reminix login --token "$TOKEN" # or: echo "$TOKEN" | reminix login
REMINIX_TOKEN=pat_… reminix whoami
```
`reminix logout` revokes the token and forgets it.
## Choose a workspace
[Section titled “Choose a workspace”](#choose-a-workspace)
```bash
reminix workspace list
reminix workspace use acme # the default for later commands
reminix workspace audit-events list --workspace other-co # one call elsewhere
```
## Every API operation is a command
[Section titled “Every API operation is a command”](#every-api-operation-is-a-command)
Each operation in the [API reference](https://api.reminix.com/v1/docs) is a command, named after its resource and action:
```bash
reminix workspace webhook-endpoints list
reminix workspace webhook-endpoints create --url https://example.com/hooks
reminix workspace webhook-deliveries list we_123 --limit 10
reminix workspace webhook-endpoints delete we_123 --yes
```
* IDs in the path are arguments; other inputs are flags (`reminix --help` lists them, with descriptions).
* `--data '{…}'` sends a whole JSON body.
* Lists print one page; the last line names the `--cursor` for the next.
* Writes send an [idempotency key](/docs/idempotency/) for you.
* An operation that cannot be undone asks for `--yes`.
* `reminix api ` calls any endpoint directly.
## Scripts and agents
[Section titled “Scripts and agents”](#scripts-and-agents)
Add `--json` to any command for the API’s exact JSON on stdout. Errors then go to stderr as JSON too — the API’s [error envelope](/docs/errors/) with the HTTP status:
```bash
reminix workspace webhook-endpoints list --json | jq '.items[].url'
```
```json
{
"error": {
"code": "forbidden",
"message": "…",
"status": 403,
"requestId": "…"
}
}
```
Exit codes: `0` success, `1` failure (any error, including a refused API call or a missing flag).
# Errors & API rate limits
> The error envelope, stable error codes, and how fast each key may call the API.
Every non-2xx response from the API uses one envelope:
```json
{
"error": {
"code": "invalid_request",
"message": "Invalid request",
"requestId": "9f0c1c2e-…",
"docs": "https://reminix.com/docs/errors/#invalid_request",
"details": [{ "path": "name", "message": "Required" }]
}
}
```
* **`code`** is a small, stable set your integration may branch on.
* **`message`** is human-readable and may change — never parse it.
* **`requestId`** matches the `X-Request-Id` response header; include it when contacting support so a request can be found in the logs.
* **`details`** appears on validation failures, with one entry per field.
* **`docs`** links to the code’s entry below — how to fix it.
## Error codes
[Section titled “Error codes”](#error-codes)
Branch on `code`, never on `message`. Every error from a public code also carries `docs`, a link to its entry below. New codes may be added over time, so treat an unknown code like `internal_error`.
### `unauthorized`
[Section titled “unauthorized”](#unauthorized)
`401` — the credential is missing, malformed, invalid, revoked, or expired. **Fix:** send `Authorization: Bearer ` with a live credential; create a new one if it was revoked or has expired.
### `forbidden`
[Section titled “forbidden”](#forbidden)
`403` — the credential is valid but not allowed to do this: it was not granted the endpoint’s [scope](/docs/authentication/#scopes), your workspace role does not allow it, or a personal access token cannot act in the workspace named by `X-Workspace`. **Fix:** use a credential with the needed access (the `message` names the missing scope).
### `not_found`
[Section titled “not\_found”](#not_found)
`404` — the resource does not exist, or is not yours to see. **Fix:** check the path and the id; ids from another workspace are not found.
### `method_not_allowed`
[Section titled “method\_not\_allowed”](#method_not_allowed)
`405` — the path exists, but not with this method. **Fix:** use one of the methods in the `Allow` response header.
### `invalid_request`
[Section titled “invalid\_request”](#invalid_request)
`400` — the request failed validation. **Fix:** read `details`, one entry per invalid field (`path` and `message`), and correct the request.
### `rate_limited`
[Section titled “rate\_limited”](#rate_limited)
`429` — the credential’s quota is exhausted ([API rate limits](#api-rate-limits)). **Fix:** wait for the `Retry-After` seconds, then retry with backoff.
### `idempotency_key_reused`
[Section titled “idempotency\_key\_reused”](#idempotency_key_reused)
`409` — this `Idempotency-Key` was already used for a different request. **Fix:** use a new key for a new operation; reuse a key only to retry the same request ([Idempotency](/docs/idempotency/)).
### `idempotency_in_progress`
[Section titled “idempotency\_in\_progress”](#idempotency_in_progress)
`409` — a request with the same `Idempotency-Key` is still running. **Fix:** wait for the `Retry-After` seconds, then retry with the same key.
### `internal_error`
[Section titled “internal\_error”](#internal_error)
`500` — something failed on our side. **Fix:** retry with backoff; if it persists, contact support with the `requestId`.
## API rate limits
[Section titled “API rate limits”](#api-rate-limits)
Every authenticated request counts against a per-credential quota (per API key on the workspace endpoints; per token on `/v1/user/*`). Exhausting it returns:
```http
HTTP/1.1 429 Too Many Requests
Retry-After: 60
```
with `code: "rate_limited"`. Honor `Retry-After` and retry with backoff; limits are per credential, so one busy integration does not starve another key’s traffic.
# Finding capabilities
> Search the team's catalog before writing something new — by words, tags or owner — and see who uses what.
The **Capabilities** page is your team’s catalog. Before writing a new script, look there: someone may have built it already.
## Search and filter
[Section titled “Search and filter”](#search-and-filter)
* **Search** as you type: words match the start of words in the name, slug, description and tags — `rev mon` finds “Monthly revenue report”.
* **Tags:** click a tag to see everything with it.
* **Mine** and **Published only** narrow the list.
The filters are in the page’s address, so you can share a filtered list. You only ever see capabilities you may use: a team’s capability stays hidden from people outside the team, in search too.
From the API (and so from agents, over MCP):
```bash
curl "https://api.reminix.com/v1/capabilities?q=refund&tag=finance&published=true" \
-H "Authorization: Bearer $REMINIX_TOKEN" -H "X-Workspace: acme"
```
`q` returns the best matches first (one page); without it the list is newest first and pages with `nextCursor`. `owner` takes a user id.
## Tags and README
[Section titled “Tags and README”](#tags-and-readme)
Set **tags** in `reminix.json` (`"tags": ["finance"]` — they replace the capability’s tags when you publish a version) or in the capability’s **Settings**. Up to 10; lowercase letters, digits and dashes.
A **README.md** in the folder you publish from is kept with that version and shown on the capability’s page while it is published — what it does, when to use it, what its inputs mean. Markdown; raw HTML is not shown.
## Who uses it
[Section titled “Who uses it”](#who-uses-it)
Every capability shows, for the last 30 days:
* **runs**, the share that succeeded, and when it last ran;
* **where runs came from** — the app, the API, the CLI, agents, schedules, triggers;
* **people** who ran it most;
* its **schedules and triggers**, and the other capabilities whose triggers run **after** it (on its `run.completed` or `run.failed`).
Before changing or retiring a capability, check who would notice.
# Governance
> Decide who can use each capability, which runs need a person's approval, and who publishes.
Every capability has an owner (whoever created it). Its owner, or a workspace owner or admin, controls three things on its page under **Settings**.
## Who can use it
[Section titled “Who can use it”](#who-can-use-it)
* **Everyone in the workspace** (the default).
* **Only these teams** — the workspace’s teams (Settings → Members). Members outside them don’t see it at all: not in the app, not through the API, not as an agent’s tool, not its runs. Owners and admins always see everything.
Guests see only what is shared with a team they are in.
Through the API: `PATCH /v1/capabilities/{slug}` with `{ "access": "teams", "teamIds": ["…"] }` or `{ "access": "workspace" }`.
## Which runs need approval
[Section titled “Which runs need approval”](#which-runs-need-approval)
| Policy | What waits for a person |
| ---------------------------- | ----------------------------------------------------- |
| **No approval** (default) | Nothing — runs start right away. |
| **Agents need approval** | Runs started by an AI agent (through the MCP server). |
| **Every run needs approval** | Every run, except by workspace owners and admins. |
A held run is recorded **Awaiting approval**; workspace owners and admins are notified and decide on its approval page. Approved, it runs — the version it was asked for, with its inputs. Denied (or not decided within a day), it is **Cancelled**. Through the API, a held run answers `202` with the run and the request’s `approveUrl`; read the run (`GET /v1/runs/{id}`) to see how it ended. An agent’s tool says it is waiting, with the link.
The policy is a setting of the capability — never part of its code — so publishing new code can’t loosen it.
## Who publishes
[Section titled “Who publishes”](#who-publishes)
Its owner, or a workspace owner or admin, publishes a version. Two cases need a person’s approval instead:
* **An AI agent’s publish** — always. The agent’s request appears for a workspace owner or admin to approve.
* **Everyone’s publish**, when the workspace turns on **Settings → Capabilities → Publishing → “Publishing needs a second person”**: the publish becomes a request that another owner or admin — not the person who asked — approves. (If the person who asked approves it themselves, it is refused; ask again for someone else.)
Every change of access, policy and setting, and every approval, is in the workspace’s audit log.
# Idempotency
> Retry writes safely with the Idempotency-Key header.
Networks fail: a request can time out after the API has already acted on it. To retry a write **without doing it twice**, send an `Idempotency-Key` header — any unique string up to 255 printable ASCII characters, such as a UUID you generate per operation:
```bash
curl -X POST \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: 6f1c2e9a-8b3d-4c47-9a51-0d2f7e4b8c13" \
-H "Content-Type: application/json" \
-d '{ … }' \
https://api.reminix.com/v1/…
```
The first request with a key runs normally. Any retry with the **same key and the same request** returns the first response again — same status, same body — without running the operation a second time, and carries the header `Idempotency-Replayed: true`.
## The rules
[Section titled “The rules”](#the-rules)
* **Writes only.** The header applies to `POST`, `PUT`, `PATCH` and `DELETE`; reads are naturally safe to repeat and ignore it.
* **One key, one request.** Reusing a key for a different request (a different endpoint or body) is a `409` with code `idempotency_key_reused`. Generate a new key per operation, and reuse it only for retries of that operation.
* **Concurrent retries wait.** A retry that arrives while the first request is still running gets a `409` with code `idempotency_in_progress` and a `Retry-After` header; retry after it.
* **Failures you can retry.** Responses with a `5xx` status are not stored, so retrying the same key runs the request again. Any other response — success or a `4xx` — is what every retry receives.
* **Per credential, for 24 hours.** Keys are scoped to the API key or token that sent them (two integrations never collide), and are remembered for 24 hours.
Sending a key is optional, and recommended on every write.
# MCP server
> Connect Claude or any MCP client to your workspace — what it can do, and how you stay in control.
AI agents that speak the Model Context Protocol (MCP) — Claude, and many other assistants and coding agents — can work in your workspace through our MCP server:
```text
https://mcp.reminix.com/mcp
```
## Connect
[Section titled “Connect”](#connect)
Add the address above as a remote MCP server (in Claude: **Settings → Connectors → Add custom connector**). The first time, your browser opens:
1. Sign in, if you are not already.
2. Choose **one workspace** the agent will work in.
3. Choose what it may do. Everything that can change data starts unticked — tick only what the agent needs.
4. Click **Allow**.
There is nothing to copy and no key to store: the agent receives its own sign-in, valid only for that workspace and that access.
## What the agent can do
[Section titled “What the agent can do”](#what-the-agent-can-do)
The agent’s tools are the [API](https://api.reminix.com/v1/docs) operations your choice allows — reading the workspace, listing webhook deliveries, and so on — always as **you**, with your role in the workspace: if you could not do something in the app, neither can the agent. Everything it changes appears in the workspace’s audit log as you, via the app.
Some things are never available to agents, even with full access — anything that sends your workspace’s data somewhere new or hands out access (for example, creating webhook endpoints or credentials). The agent can **ask** for them: you approve or deny in the app, and any secret is shown to you, never to the agent — see [Approvals](/docs/approvals/).
## Stay in control
[Section titled “Stay in control”](#stay-in-control)
* **Account → Connected apps** lists every agent you connected; disconnect one and it stops working immediately.
* Your workspace’s **audit log** records what each agent did.
* Removing you from the workspace cuts off your agents too.
## For developers
[Section titled “For developers”](#for-developers)
The server follows the MCP authorization specification: an unauthenticated request gets a `401` pointing at `/.well-known/oauth-protected-resource/mcp`, which names the authorization server (`https://api.reminix.com/auth`, with dynamic client registration and PKCE). Tokens are issued for the MCP server alone. Prefer the [command line](/docs/cli/) or the [SDK](/docs/sdk/) for your own scripts, and see [Use with AI agents](/docs/agents/) for the agent skill.
# Pagination
> Cursor pagination on list endpoints.
List endpoints use **cursor pagination**: results come newest-first in pages, with an opaque cursor pointing at the next page.
```bash
curl \
-H "Authorization: Bearer YOUR_API_KEY" \
"https://api.reminix.com/v1/workspace/audit-events?limit=25"
```
```json
{
"items": [{ "id": "…", "action": "member.invited", "createdAt": "…" }],
"nextCursor": "eyJ0IjoiMjAyNi0wOC0uLi4ifQ"
}
```
To fetch the next page, pass the cursor back unchanged:
```bash
curl \
-H "Authorization: Bearer YOUR_API_KEY" \
"https://api.reminix.com/v1/workspace/audit-events?limit=25&cursor=eyJ0IjoiMjAyNi0wOC0uLi4ifQ"
```
Iterate until `nextCursor` is `null` — that is the last page.
Rules worth knowing:
* `limit` accepts 1–100 (default 25).
* Cursors are **opaque**: never construct or modify one; a malformed cursor returns `invalid_request`.
* Pages are stable under concurrent writes — new rows created while you paginate never cause skips or duplicates within your walk.
* Treat the response shape as additive: new fields may appear on items, existing ones will not be renamed.
# Quickstart
> From zero to your first authenticated API call.
## 1. Sign up and create a workspace
[Section titled “1. Sign up and create a workspace”](#1-sign-up-and-create-a-workspace)
Create your account in the application, verify your email, and create your first workspace during onboarding. Everything in the API belongs to a workspace.
## 2. Create an API key
[Section titled “2. Create an API key”](#2-create-an-api-key)
As a workspace owner, go to **Settings → API Keys**, create a key, and copy the secret — it is shown exactly once. Store it in your secret manager, never in code.
## 3. Make your first call
[Section titled “3. Make your first call”](#3-make-your-first-call)
```bash
curl \
-H "Authorization: Bearer YOUR_API_KEY" \
https://api.reminix.com/v1/workspace
```
```json
{ "id": "…", "name": "Your Workspace" }
```
That response is the API’s “who am I” — a healthy integration smoke test.
## Next steps
[Section titled “Next steps”](#next-steps)
* [Authentication](/docs/authentication/) — how keys and personal access tokens work, and how to rotate them.
* [Webhooks](/docs/webhooks/) — push events to your systems.
* The [TypeScript SDK](/docs/sdk/) and the [command line](/docs/cli/).
* [Errors & API rate limits](/docs/errors/) and [Pagination](/docs/pagination/).
* The **interactive API reference** — every endpoint, schema, and a try-it console — lives at [`api.reminix.com/v1/docs`](https://api.reminix.com/v1/docs), generated from the same definitions that validate requests, so it is always current.
# Runs
> Run a published capability from the API or the command line, and read its output and log.
A **run** is one execution of a capability’s **published** version. Every run is recorded: its inputs, the version it used, who started it and through what (the app, the API, the CLI or an agent), how it ended, its output or error, and its log.
## Running a capability
[Section titled “Running a capability”](#running-a-capability)
In the app, open the capability, fill in its form and click **Run**. From the command line, `reminix capabilities run --input ''`. Agents connected through the [MCP server](/docs/mcp/) see a tool per published capability, `run_`. Through the API, with a key or token holding `runs:write`:
```bash
curl -X POST https://api.reminix.com/v1/capabilities/refund-customer/runs \
-H "Authorization: Bearer $REMINIX_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "inputs": { "orderId": "o_123" } }'
```
The answer comes when the run finishes:
```json
{
"id": "run_7b1a…",
"capability": { "id": "cap_3f0c…", "slug": "refund-customer" },
"version": 3,
"status": "succeeded",
"inputs": { "orderId": "o_123" },
"output": { "refunded": "o_123" },
"error": null,
"interface": "api",
"logLines": 1,
"durationMs": 42
}
```
A run that throws is `"status": "failed"` with its `error` — still `200`, because the run happened. Read what it logged with `GET /v1/runs/{id}/logs`.
## Inputs are checked first
[Section titled “Inputs are checked first”](#inputs-are-checked-first)
The inputs must match the version’s `inputSchema`. When they do not, the run does not start: `400 invalid_request` with one entry per problem in `details`:
```json
{
"error": {
"code": "invalid_request",
"message": "The inputs do not match the capability's input schema",
"details": [
{
"path": "/orderId",
"message": "Instance type \"number\" is invalid. Expected \"string\"."
}
]
}
}
```
## Where the code runs
[Section titled “Where the code runs”](#where-the-code-runs)
Each version runs in its own isolate, with no access to other capabilities or to your workspace — only its inputs. It has **no network access** unless its version declares `hosts`, and then it reaches only those, over HTTPS, at most 50 requests a run, with its secrets attached by Reminix ([Capabilities → Calling APIs](/docs/capabilities/#calling-apis)). Every request appears in the run’s log. A run may use up to 30 seconds; longer runs fail with a time-out.
A run whose secrets cannot be used — one is not set, may not be sent to that host, or may not be read by code — fails before it starts, and the run says why.
## History and usage
[Section titled “History and usage”](#history-and-usage)
`GET /v1/runs` lists runs, newest first (`?capability=` for one capability); `GET /v1/runs/{id}` reads one. Every run counts toward your workspace’s **runs** usage (the Free plan includes 1,000 runs a month).
Webhook events: `run.completed`, `run.failed`, and `capability.published` when a new version goes live.
# Schedules and triggers
> Run a capability on a timetable, or when something happens in your workspace.
A capability can run without anyone clicking Run — on a **schedule**, or on a **trigger**. Either way the run is recorded like any other, marked **Schedule** or **Trigger**, and acts as **the person who created it**: with their access, counted on the workspace’s runs.
## Schedules
[Section titled “Schedules”](#schedules)
On the capability’s page, **Schedules → Add schedule**: a name, when (every hour, every day at 9:00, weekdays at 9:00, every Monday — or any 5-field cron), a timezone, and the inputs (the capability’s own form). Runs are at least 5 minutes apart and follow the timezone’s clock, daylight saving included.
```bash
curl -X POST https://api.reminix.com/v1/capabilities/nightly-report/schedules \
-H "Authorization: Bearer $REMINIX_TOKEN" -H "X-Workspace: acme" \
-H "Content-Type: application/json" \
-d '{ "name": "Weekday mornings", "cron": "0 9 * * 1-5",
"timezone": "Europe/Berlin", "inputs": { "team": "sales" } }'
```
## Triggers
[Section titled “Triggers”](#triggers)
**Triggers → Add trigger**: the event (any workspace event — another capability’s `run.completed` or `run.failed`, `capability.published`, a member joining…), optionally **only when** a field of the event equals a value, and the inputs. A value `{{event.data.}}` is taken from the event:
```json
{
"name": "Alert on failure",
"eventType": "run.failed",
"filter": { "capability.slug": "nightly-report" },
"inputs": { "runId": "{{event.data.runId}}", "error": "{{event.data.error}}" }
}
```
A trigger starts its run within about a minute of the event. A chain of triggered runs (a run whose completion triggers another…) stops after 3, so a capability can never trigger itself forever.
## Who can, and when they stop
[Section titled “Who can, and when they stop”](#who-can-and-when-they-stop)
* Anyone who may run a capability can schedule or trigger it — as themselves. Its creator, or a workspace owner or admin, changes or deletes it. For a capability whose every run needs approval, only owners and admins can (they are the approvers).
* They are created by a person — in the app, or with a person’s token. An AI agent can list them, but asks you to create them; a workspace API key cannot (there is no person to act as).
* If its creator leaves the workspace or loses access to the capability, a schedule or trigger is **switched off**, and its card says why. Switch it back on (or recreate it) as someone who has access.
* If a run is refused before it starts (no published version, inputs that no longer fit a new version), that time is skipped; the schedule stays on.
# TypeScript SDK
> Call the API from TypeScript with types generated from the API itself.
The SDK is a small typed client: every path, parameter and response is typed from the same definitions that validate requests, so it matches the [API reference](https://api.reminix.com/v1/docs) exactly.
## Install
[Section titled “Install”](#install)
```bash
npm install @reminix/sdk
```
## Make a call
[Section titled “Make a call”](#make-a-call)
```ts
import { createClient, unwrap } from "@reminix/sdk";
const api = createClient({ token: process.env.API_KEY! });
const endpoints = unwrap(await api.GET("/workspace/webhook-endpoints"));
for (const endpoint of endpoints.items) console.log(endpoint.url);
```
* With an **API key**, every call acts in the key’s workspace.
* With a **personal access token**, name the workspace: `createClient({ token, workspace: "acme" })`.
* `unwrap` returns the data or throws an `ApiError` carrying the [error envelope](/docs/errors/)’s `code`, `message`, `status` and `requestId`.
* Writes may send an [idempotency key](/docs/idempotency/): `api.POST("/workspace/webhook-endpoints", { body, headers: { "idempotency-key": id } })`.
# Secrets
> Store API keys your capabilities use — encrypted, write-only, and sent only where you allow.
A **secret** is an API key or token your capabilities use — a Stripe key, a Slack token. Reminix keeps it so the code never has to:
* **Write-only.** You save a value; nothing — the app, the API, the command line, an agent — ever shows it again. Rotate it by saving a new value.
* **Encrypted at rest**, with a key per workspace.
* **Sent only to its hosts.** Each secret lists the hosts it may be sent to (`api.stripe.com`). Reminix attaches it to a capability’s requests to those hosts — and to nothing else, whatever the code says.
* **Not readable by code**, unless you turn on “Let capabilities read the value itself” for that secret.
Owners and admins save, rotate and delete secrets. Members see their names and hosts (to write manifests), never their values.
## Saving one
[Section titled “Saving one”](#saving-one)
In the app: **Settings → Secrets → Add secret**. From the command line, the value comes from standard input — never an argument, which would land in your shell history:
```bash
printf %s "$STRIPE_KEY" | reminix secrets set STRIPE_KEY --hosts api.stripe.com
```
Through the API, with a key holding `secrets:write`:
```bash
curl -X PUT https://api.reminix.com/v1/secrets/STRIPE_KEY \
-H "Authorization: Bearer $REMINIX_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "value": "sk_live_…", "hosts": ["api.stripe.com"] }'
```
`GET /v1/secrets` lists names and hosts; `DELETE /v1/secrets/{name}` deletes one. Agents can list secrets but never save or delete them — an agent that needs a secret asks you to add it.
## Using one
[Section titled “Using one”](#using-one)
A capability declares the secrets it uses in its `reminix.json` — see [Capabilities → Calling APIs](/docs/capabilities/#calling-apis). A run uses a secret only if the secret allows the host it is sent to; saving a new value (rotation) reaches the next run.
# Versioning
> What can change in the API, what never changes, and how deprecations are announced.
The API is versioned in its path: `/v1`. Within a version we only make changes that do not break a correct integration — and the same promise covers the command line and agent tools built from the API, whose names come from the API’s operations.
## Changes we may make at any time
[Section titled “Changes we may make at any time”](#changes-we-may-make-at-any-time)
* New endpoints.
* New optional request parameters and fields.
* New fields in responses.
* New error codes, new webhook event types, new scopes.
* New commands and tools for the command line and agents.
Build your integration to tolerate these:
* **Ignore response fields you do not recognize.**
* **Treat an unknown error `code` like `internal_error`**, and branch on `code`, never on `message`.
* **Treat cursors and ids as opaque** strings.
## Changes we never make within `/v1`
[Section titled “Changes we never make within /v1”](#changes-we-never-make-within-v1)
* Removing or renaming an endpoint, a field, or an operation (and so a command or an agent tool).
* Changing the type or meaning of an existing field.
* Making an optional parameter required.
* Changing how requests authenticate, or which error a given failure returns.
A change like these ships only in a new version (`/v2`), served alongside `/v1` while integrations move.
## Deprecations
[Section titled “Deprecations”](#deprecations)
Before anything in `/v1` is retired, it is announced in the changelog and marked **deprecated** in the [API reference](https://api.reminix.com/v1/docs), with the replacement to use. Deprecated endpoints keep working for the life of `/v1`.
# Webhooks
> Receive workspace events at your endpoint, verify signatures, and handle retries.
Webhooks push workspace events to an HTTPS endpoint you host, as they happen. Workspace owners manage endpoints in the application under **Settings → Webhooks** — add a URL, copy the signing secret (shown exactly once), and events start flowing.
## Managing endpoints with the API
[Section titled “Managing endpoints with the API”](#managing-endpoints-with-the-api)
Software can manage endpoints too, with a credential granted the `workspace.webhooks:write` scope (reading needs `workspace.webhooks:read`; a personal access token must also belong to a workspace owner):
```bash
curl -X POST \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{ "url": "https://example.com/hooks" }' \
https://api.reminix.com/v1/workspace/webhook-endpoints
```
The response carries the endpoint’s signing secret — once. The API also lists endpoints, re-enables one after repeated failures, sends a test event, and lists and redelivers deliveries; see the [API reference](https://api.reminix.com/v1/docs).
AI agents connected to the workspace can read endpoints and delivery history, send test events and redeliver — but not add, remove or re-enable endpoints: those change where your workspace’s data is sent, so a person configures them.
## The delivery
[Section titled “The delivery”](#the-delivery)
Each delivery is an HTTP `POST` with a JSON body:
```json
{
"id": "evt_5f0c…",
"type": "workspace.member.joined",
"createdAt": "2026-08-20T12:00:00.000Z",
"data": { "userId": "…", "email": "casey@example.com" }
}
```
and three signature headers:
```http
webhook-id: del_9a1b…
webhook-timestamp: 1755691200
webhook-signature: v1,MEQCIB…
```
* **`webhook-id`** identifies this delivery. It stays the same across retries — use it to deduplicate.
* **`id`** inside the body identifies the *event*. If you registered multiple endpoints, each receives its own delivery of the same event — deduplicate across endpoints by event `id` if you need to.
## Verifying signatures
[Section titled “Verifying signatures”](#verifying-signatures)
Deliveries are signed with your endpoint’s secret (`whsec_…`) using the same scheme as [Svix](https://docs.svix.com/receiving/verifying-payloads/how), so any standard Svix library verifies them:
```ts
import { Webhook } from "svix";
const wh = new Webhook(process.env.WEBHOOK_SECRET);
// Express-style handler; `payload` must be the RAW request body string.
app.post("/webhooks", (req, res) => {
let event;
try {
event = wh.verify(req.body, req.headers);
} catch {
return res.status(400).send("bad signature");
}
// handle event…
res.status(200).send("ok");
});
```
Verifying by hand: the signature is `v1,` followed by a Base64 HMAC-SHA256 of `` `${webhookId}.${timestamp}.${body}` `` keyed with the secret after its `whsec_` prefix (Base64-decoded). Always verify against the **raw** body — re-serializing JSON breaks the signature — and reject timestamps older than a few minutes to prevent replays.
## Respond fast, process later
[Section titled “Respond fast, process later”](#respond-fast-process-later)
Return a `2xx` within 10 seconds. If your processing is slow, acknowledge first and process asynchronously — a timeout counts as a failed delivery.
## Retries and failures
[Section titled “Retries and failures”](#retries-and-failures)
Failed deliveries (non-2xx, or timeout) retry automatically with backoff, up to 6 attempts, with the same `webhook-id`. An endpoint that fails **20 consecutive** deliveries is disabled automatically — delete and re-add it (new secret) once your endpoint is healthy.
Under **Settings → Webhooks → Deliveries** you can see each delivery’s status and attempts, send a test event (`type: "ping"`), and **redeliver** any recorded delivery — a redelivery arrives with a fresh `webhook-id` but the same event `id`, so event-level deduplication still applies.
## Events
[Section titled “Events”](#events)
| Type | Fires when | `data` |
| ------------------------- | ------------------------------ | ----------------- |
| `workspace.member.joined` | An invitation is accepted. | `userId`, `email` |
| `ping` | You send a test from Settings. | a test message |
Event payloads only ever gain fields — build tolerant parsers. `GET /v1/workspace/event-types` returns the full list, with what each means.
## Choosing events
[Section titled “Choosing events”](#choosing-events)
An endpoint receives **every** event unless you choose: in **Settings → Webhooks** pick “Only these” when adding it, or pass `eventTypes` when creating it through the API — only listed types are accepted:
```bash
curl -X POST https://api.reminix.com/v1/workspace/webhook-endpoints \
-H "Authorization: Bearer YOUR_API_KEY" -H "content-type: application/json" \
-d '{"url":"https://example.com/hooks","eventTypes":["workspace.member.joined"]}'
```
A test event (`ping`) always reaches the endpoint you test.
# Workspace usage
> How much of your plan the workspace used this month, and the limits.
Some things are counted per calendar month (UTC) — each meter, with your plan’s limit for it. See them under **Settings → Billing**, or through the API:
```bash
curl -H "Authorization: Bearer YOUR_API_KEY" \
https://api.reminix.com/v1/workspace/usage
```
```json
{
"periodStart": "2026-10-01T00:00:00.000Z",
"periodEnd": "2026-11-01T00:00:00.000Z",
"meters": [
{
"id": "webhook_deliveries",
"description": "Webhook deliveries this month (each event, each endpoint)",
"used": 1520,
"limit": null
}
]
}
```
`limit` is `null` when the plan has no limit for that meter. Counters reset at the start of each month. The key needs the `workspace:read` scope.