Silicon Remind DocsOpen Remind ↗
SILICON REMIND / DOCUMENTATION

Public HTTP API#

The production origin is https://api.remind.teamofsilicons.com. Routes begin with /api/v2 (API contract 2); every request names it with X-Remind-API-Version: 2. JSON requests use Content-Type: application/json. Errors look like {"error":{"code":"…","message":"…","hint":"…","request_id":"…"}} (hint when there is a next step); quote the request id when reporting a problem. The OpenAPI document is openapi.yaml.

Contract 1 (/api/v1) is retired: every /api/v1 path answers 410 api_version_retired with a pointer to v2.

Authentication#

Send a Silicon Accounts access token issued to Remind:

Authorization: Bearer <access token>
X-Remind-API-Version: 2

The token is an EdDSA JWT with aud = remind, checked against Silicon Accounts' published keys (iss, aud, expiry). Before actions that cannot be undone or that reveal a secret (archiving a reminder, adding or ending webhook subscriptions, sharing, allow-lists, test environment keys, retiring an environment), Remind also asks Silicon Accounts whether the token is still active. Sign-in happens between your client and Silicon Accounts; Remind never sees a refresh token:

  • Carbons: the device flow (POST {ACCOUNTS}/v1/device/authorize with client_id=remind, then poll POST {ACCOUNTS}/v1/oauth/token), as remind login does.
  • Silicons: a short-lived token from silicon-accounts login --app remind -q, exchanged at POST {ACCOUNTS}/v1/oauth/token with grant_type=urn:silicon:params:oauth:grant-type:slt, slt and client_id=remind.
  • Websites: the hosted sign-in pages and a code exchange with Remind's app secret, on a server.

Signing in and who sees what explains both flows and the rules below.

401 code meaning
unauthenticated no credential, or one Remind cannot read
token_expired the access token expired; refresh it
token_revoked issued before the account signed out, was removed or deleted; sign in again
token_wrong_audience issued to another app; another app reads with a verification proof instead
token_kind_mismatch the token's kind differs from what Remind knows for that account
legacy_token_rejected a token from contract 1 (oat_/ort_); sign in with Silicon Accounts
proof_as_bearer a sap_ proof sent as Bearer; send it as Proof
account_deleted the account was deleted

Every 401 carries WWW-Authenticate: Bearer realm="remind".

Other apps: verification proofs#

Another app reads on an account's behalf with a Silicon Accounts User verification proof: Authorization: Proof sap_…. Remind verifies it with Silicon Accounts and requires that it is valid, a User verification proof for receiving app remind, carries the scope remind.schedules.read, and comes from an app the deployment allows for that scope (REMIND_PROOF_ISSUERS). It is accepted only on GET /schedules, GET /schedules/{id}, GET /schedules/{id}/executions, GET /silicons and GET /auth/me (elsewhere 401 proof_not_accepted), and reads exactly what that account may read.

Who may do what#

Every reminder belongs to the Silicon that created it. Accounts are identified by their Silicon Accounts uuid (a permanent 128-bit UUIDv4 in canonical lowercase, hyphenated form) and shown by their current c:/si: id.

  • A Silicon reads its own reminders and those of its custodian's other Silicons, and changes only its own.
  • A Carbon reads the reminders of the Silicons it looks after (it is their custodian). Carbons never create, change, pause or archive reminders, and never subscribe webhooks.
  • Anyone reads what a Silicon (or its custodian) shared with them (/viewers).
  • Sharing with a Silicon outside the owner's custodian and its other Silicons needs that Silicon (or its custodian) to have allowed the owner first (/allowed-accounts).

A reminder you cannot read answers 404, as if it did not exist.

The calling account#

Method and path Result
GET /auth/me {uuid, kind, id, display_name, pfp_url, custodian, can_manage_reminders, credential, issuing_app, visible_silicons}
GET /silicons?after=<uuid>&limit=50 The Silicons whose reminders you can read: {uuid, silicon_id, display_name, pfp_url, relation, reminder_count}; relation is self, custodian, sibling or shared

credential is access_token or proof (then issuing_app names the app). /silicons pages by uuid: pass next_cursor as after.

Reminders#

Method and path Required input Result
POST /schedules text, kind, cron, timezone; Idempotency-Key 201 reminder
GET /schedules optional filters below page of reminders
GET /schedules/{id} reminder id the reminder
PATCH /schedules/{id} changed fields; Idempotency-Key updated reminder
PATCH /schedules schedule_ids, status; Idempotency-Key results in the order given
DELETE /schedules/{id} owner Silicon 204; archives it
GET /schedules/{id}/executions optional cursor, limit delivery history
{
  "text": "Check the build results",
  "kind": "recurring",
  "cron": "*/15 * * * *",
  "timezone": "Asia/Kolkata"
}

kind is recurring or one_time. Both use five-field Linux cron: minute, hour, day of month, month, day of week. One-time means the first future match; the reminder then moves to the archive. An IANA timezone is mandatory on creation ("timezone": "UTC" for UTC); a missing, null or blank one answers 422 timezone_required, and unknown identifiers, display names and fixed offsets are refused.

Cron supports Linux/Vixie lists, ranges, steps and named months and weekdays. Sunday is 0 or 7. When both day of month and day of week are restricted, either may match. Quartz extensions (L, W, #) are refused. A wall-clock time skipped by a DST change is skipped; both real instants in a repeated hour are eligible. The next occurrence is recalculated in UTC after each trigger.

Text must be non-blank and at most 100,000 UTF-8 bytes. A reminder carries id, owner ({uuid, id, kind}), silicon_id (the owner's current id), text, kind, cron, timezone, status, section, next_run_at, archived_at, purge_after, created_at and updated_at.

Listing filters: silicon_id (si: id or uuid), section=current|archived, status=active|paused|completed, cursor, limit (1 to 100). current is the default. Pass next_cursor unchanged with the same filters for the next page.

PATCH accepts text, timezone, kind, cron and status; omitted fields are unchanged and cron cannot be cleared. Timing changes recalculate the next occurrence; a text-only change keeps it. Only active and paused can be written; completed is set when a one-time reminder fires. Archived reminders cannot change.

Pause or resume 1 to 100 distinct reminders at once:

{"schedule_ids":["0198f74d-7ef7-7c9f-95bf-7d403a61e5ca"],"status":"paused"}

Every id must be a current reminder of the caller, or the whole batch fails. Pausing stops future occurrences; resuming computes the next future match; repeating the same state changes nothing. Reuse an Idempotency-Key (16 to 255 visible ASCII characters) only with the exact same request; a different body answers 409 idempotency_conflict.

Sharing and allow-lists#

Method and path Who Result
GET /viewers anyone {granted, received}: grants on your (or your Silicons') reminders, and grants you hold
POST /viewers the Silicon, or its custodian {id, silicon_id?} → 201 grant (200 when it already existed)
DELETE /viewers/{viewer}?silicon_id= the Silicon, or its custodian 204
GET /allowed-accounts?silicon_id= the Silicon, or its custodian {items}
POST /allowed-accounts the Silicon, or its custodian {id, silicon_id?} → 201 entry (200 when it already existed)
DELETE /allowed-accounts/{account}?silicon_id= the Silicon, or its custodian 204; also ends the grants the entry made possible

id names the other account by c:/si: id or uuid (Remind looks it up at Silicon Accounts). A Carbon names which of its Silicons the request is about with silicon_id; a Silicon may omit it. A grant is {id, owner, viewer, granted_by, created_at} and an allow-list entry {id, silicon, allowed, created_by, created_at}, accounts as {uuid, id, kind}. Granting to a Silicon outside the owner's custodian and its other Silicons answers 403 silicon_not_open until that Silicon allows the owner (or the granting custodian). Both are refused inside a test environment (403 not_in_test_environment).

Webhook subscriptions#

Method and path Behavior
POST /webhooks Add a subscription for the calling Silicon
GET /webhooks?silicon_id= A Silicon's own subscriptions, or (read-only, for a Carbon) those of the Silicons it looks after
DELETE /webhooks/{subscription_id} End one subscription; 204
PUT /webhook Older single-endpoint form of POST /webhooks
GET /webhook The first active subscription, without the signing secret
DELETE /webhook End every subscription of the calling Silicon; 204

Input is {"endpoint_url":"…","signing_secret":"…"}; leave out signing_secret for unsigned delivery. The endpoint must be an absolute https URL in production (credentials and fragments are refused). Endpoint and secret are encrypted at rest and the secret is never returned. A receipt is {id, silicon_id, silicon_uuid, version, updated_at}. Subscriptions are optional: reminders created without any simply have nowhere to go until one is added. See the delivery contract.

Delivery, archive and retention#

When a reminder comes due, the worker stores an immutable occurrence, then posts it to every subscription. The execution id is stable across retries and is the delivery's idempotency key. The snapshot holds the text at trigger time, the reminder and owner identity, the timezone and the intended instant. Passing failures retry with bounded backoff; final failures stay in the execution history. Reminders of an account that removed Remind from its apps stop firing until it signs in again.

The owner can archive a reminder at any time; a one-time reminder enters the archive when it fires. Archived reminders and their history stay readable for 45 days. Before a reminder is removed for good, one JSON line with the reminder, trigger and creator snapshots is written to the deleted-reminders log, which keeps the newest 100,000 records. The log is internal and not exposed through the API.

Test environments#

See the testing guide. Management is a production action with your access token (a request carrying X-Remind-Test-Key here answers 403 test_key_not_allowed_here):

Method and path Result
POST /test-environments {name, description?} → 201 {environment, key}
GET /test-environments page; include_deleted, after (environment id), limit 1 to 100
GET /test-environments/{id} metadata: {id, owner_uuid, owner, name, description, version, created_at, last_activity_at, deleted_at, purge_after}
GET /test-environments/{id}/key {environment_id, key}
POST /test-environments/{id}/key-rotations a new key; the old one stops working
DELETE /test-environments/{id} 204; retired, restorable for 30 days
POST /test-environments/{id}/restorations restored, with a new key

Use an environment by adding X-Remind-Test-Key: <key> to ordinary requests, with your normal access token. GET /testing-environment and POST /testing-environment/cleanings (204) work with the key alone; without a key they answer 400 test_environment_required. Owners and, for a Silicon owner, its custodian manage an environment; the owner's custodian and its other Silicons (or a Carbon owner's Silicons) see it and read its key; anyone with the key uses and cleans it. An environment holds at most 100 reminders (409 test_reminder_limit), retires after 15 days without activity, and is restorable for 30 days. Sending the test key or app secret fields that contract 1 took answers 422 test_key_field_retired.

Errors and operational endpoints#

401 means no usable credential (codes above). 403 means a known account lacks the permission (not_reminder_owner, silicon_only, not_custodian, not_environment_manager, proof_cannot_write…). 404 also hides what you cannot read. 409 reports lifecycle or idempotency conflicts, duplicate active environment names and the test environment reminder limit. 410 api_version_retired answers contract 1 paths. 422 reports invalid data. 429 may include Retry-After. 503 means Silicon Accounts or the database could not answer safely; it never becomes a guessed identity.

/health/live reports process liveness and /health/ready database readiness (no credential). /metrics is operational; restrict it by deployment networking. POST /webhook/ (also /webhook) receives Silicon Accounts' signed account events (id and profile changes, custodian changes, sign-outs, access removal, deletion); it is not the subscription route /api/v2/webhook.

Contract negotiation#

GET /api/versions lists the contracts this server serves and their state. Send X-Remind-API-Version: 2 with /api/v2 requests; an unsupported or conflicting selection answers 406 before anything runs, and the selected version comes back on the response. See the version policy.

Reports and telemetry#

POST /api/v2/reports takes {"message":"steps, expected result, actual result","pr":null} with your access token and an Idempotency-Key. Messages are at most 16,384 UTF-8 bytes, at most 10 new reports per account per hour, and pr must link a pull request of this repository. It answers 202 {id, status, failure_reason}; replaying the same key and body answers 200 with the same receipt, a different body 409. GET /api/v2/reports/{id} is visible only to the account that sent it. Without the email transport configured, production answers 503; inside a test environment reports are simulated.

POST /api/v2/telemetry/events takes only the bounded TelemetryEvent schema, with your access token. X-Remind-Telemetry: off turns observations off for any request. See diagnostics.