Developer reference

The BrainCrumb API

Anything that can make an HTTP request can capture a thought: a watch, a shortcut, a shell script, a machine in your shop.

Base URLhttps://api.braincrumb.app
Quickstart

Your first capture, in about a minute.

  1. Create a key

    In the web app, under Settings → For developers → Devices & keys, create an API key. It looks like bc_sk_… and is shown once, so copy it then.

  2. Copy the curl

    Swap in your key and run it. That rawText field is the whole required payload.

    POST /captures
    curl -X POST https://api.braincrumb.app/captures \
      -H "Authorization: Bearer bc_sk_YOUR_KEY" \
      -H "Content-Type: application/json" \
      -d '{"rawText": "Idea: braincrumb sticker on the coffee machine"}'
  3. You're capturing

    Open the app: the thought is already filed as an idea, entities extracted, ready to search. Everything below this point is detail.

Pebble Index 01

Talk to your wrist. It lands filed.

The Pebble app transcribes your voice notes on your phone and can forward each one to a webhook. Point that webhook at BrainCrumb and every note you mumble into your ring arrives classified and searchable. No companion app, no glue code.

The grammar works anywhere BrainCrumb listens: say “Remind me to…” for a reminder, or track time with “Start youtube”, “End youtube”, and “End last activity”. Sessions roll up in the Time view.

  1. Create a send key (Send captures) in Settings → For developers → Devices & keys.

  2. In the Pebble phone app, find the voice-note webhook setting. The wording moves around between versions; look for “webhook” or “send transcripts to a URL.”

  3. Give it the URL, the Authorization header, and a JSON body that puts the transcript in rawText. Done. The server does the filing.

Nothing here is Pebble-specific. The same three lines turn an Apple Shortcut into a capture button, a cron job into a system logger, or any script into a thought inbox. If it can POST JSON, it can capture.

Webhook configuration
In the Pebble app: Index Settings -> Webhook

Webhook URL   https://api.braincrumb.app/captures
Headers       Authorization: Bearer bc_sk_YOUR_KEY
Send          Transcription only (or Both to keep the audio)

The app's native multipart payload (transcription, audio,
recordedAt) is accepted as-is. Duplicate resends are deduped
automatically, and offline recordings keep their original
capture time. JSON clients can send
{"rawText": "<text>"} instead. Both work.

On flaky radio, add an Idempotency-Key header per note so retries never duplicate a thought. See the reference below.

Reference

Three endpoints do most of it.

Every request authenticates with an API key in the Authorization: Bearer bc_sk_… header. A key is one of three kinds, chosen when it is made. A missing or bad key gets a 401; a key without the needed scope gets a 403.

  • A send key (write) sends captures and reads back in full only the captures it sent. Of any other capture it is given the id, whether it is done or archived, its collections and its tags, and on any capture it can change those and nothing else. It can add or change people, calendars, events and collections, and is told only that it did. It cannot list, search, export or delete.
  • A read key lists, searches and exports captures, reads any one of them in full, and reads people, calendars, events and collections. It changes nothing.
  • A full key does all that a send key and a read key do, reads every capture in full, and can also delete captures, people, calendars, events and collections.

The three below are documented in full. Within what its kind allows, a key can also read or change one capture (GET and PUT /captures/:id), count captures (GET /captures/counts), export them (GET /export), and read or change people, calendars, events and collections (/contacts, /calendars, /events, /collections). A refused request is answered with the full list.

POST/capturesWrite scope

Create a capture. Send the thought as text; if you leave category out, the server classifies it: files it as one of the thirteen kinds listed below (a to-do, a reminder, an event, a contact, an expense and so on) and extracts the useful parts (names, dates, places) into extractedData.

Body · application/json

FieldTypeDescription
rawTextrequiredstringThe thought, as text. Required unless transcription is present.
transcriptionstringAlready-transcribed speech, if your device did the transcribing. Treated like rawText.
categorystringOne of contact event todo idea note link image reminder activity expense quote media health. Omit it and the server decides.
capturedAtISO 8601When the thought happened. Defaults to arrival time.
latitude, longitudenumberWhere it happened. Send both or neither; captures with coordinates show up on the map.
locationNamestringA human-readable place, e.g. “Grant Park”.

Context is whatever you send. The API stores these fields as given, and for a capture sent with a key there is no geocoding or weather lookup on our side. One exception: a capture that arrives with no coordinates is given the place your phone last reported through the BrainCrumb app, when that was recent enough that the phone cannot have moved far, and its place name then starts with “Near”. The BrainCrumb app attaches place, weather and motion because it reads them from the phone at the moment of capture; a headless device has no sensors to read, so its captures arrive without them unless it sends some.

Headers

HeaderTypeDescription
AuthorizationrequiredstringBearer bc_sk_…
Idempotency-KeystringAny unique string (up to 128 chars) per capture attempt. Retrying with the same key returns the original capture instead of creating a duplicate. Built for devices on flaky connections that retry until they hear back. Use a new one for each capture. A new capture sent with a key already used is not saved: the answer is the first one.
201 Created
{
  "id": "5b21c6a5-8f4e-4c9a-9d3e-2f7a1c0b8d44",
  "rawText": "Idea: braincrumb sticker on the coffee machine",
  "category": "idea",
  "confidence": 1,
  "isProcessed": true,
  "extractedData": { "rawEntities": { "phones": [], "emails": [], "urls": [], "dates": [], "addresses": [] } },
  "capturedAt": "2026-08-05T14:31:07.412Z",
  "locationName": null,
  "updatedAt": "2026-08-05T14:31:07.412Z"
}

Responses

  • 201

    Created. The full capture, classified and filed.

  • 200

    Replayed. This Idempotency-Key already landed; you got the original capture back (with an Idempotency-Replayed: true header).

  • 400

    Bad request. Nothing to capture (the server says: At least one of rawText, transcription, imageKey, or url is required), a body that is not a JSON object, or a category that is not one of the thirteen.

  • 401

    Unauthorized. Missing, malformed, or revoked key.

  • 403

    Forbidden. The key exists but lacks write scope.

  • 429

    Daily cap reached. While BrainCrumb is in beta each key can send 2,000 captures a day. The day turns at midnight in your account’s time zone (UTC when it has none). The response carries a Retry-After header (seconds). Back off, don’t hammer.

GET/capturesRead or full scope

List captures, newest first. All query parameters are optional and combine.

Query parameters

ParamTypeDescription
categorystringFilter to one of contact event todo idea note link image reminder activity expense quote media health.
searchstringFinds captures that hold every word you give, in any order, in the text, transcription, place name, address or tags. For meaning, use /search.
limitintegerPage size, 1–200. Default 50.
offsetintegerSkip this many results. Default 0.
from, toISO dateBound capturedAt, inclusive, e.g. from=2026-08-01&to=2026-08-05.
200 OK · fields trimmed
{
  "captures": [
    {
      "id": "1f7e9d02-6c3b-4a51-b8e4-90d2c5a7f316",
      "rawText": "Call Dana about the mural wall",
      "category": "todo",
      "capturedAt": "2026-08-04T22:10:03.918Z",
      "locationName": "Grant Park"
    }
  ],
  "total": 132
}

Responses

  • 200

    OK. captures is the page; total is the full match count for pagination.

  • 401

    Unauthorized. Missing, malformed, or revoked key.

  • 403

    Forbidden. A send key cannot list captures. Listing needs a read key or a full key.

GET/search?q=Read or full scope

Hybrid search: exact keyword matches and semantic matches, merged. Results that match both ways rank first, then keyword, then semantic, so a literal match is never buried under a vibe, but “that pizza place someone mentioned” still surfaces.

Query parameters

ParamTypeDescription
qrequiredstringWhat you remember, in your own words.
limitintegerMax results, 1–100. Default 30.
200 OK · fields trimmed
{
  "results": [
    {
      "capture": {
        "id": "8a4c1e77-2b90-4f6d-a3c8-51e0b9d64f22",
        "rawText": "That pizza place Marcus swears by, near the Beltline",
        "category": "note"
      },
      "score": 0.83,
      "matchType": "both"
    }
  ],
  "semanticEnabled": true
}

Responses

  • 200

    OK. Each result carries the capture, a relevance score, and a matchType of keyword, semantic, or both.

  • 400

    Bad request. q is required.

  • 401

    Unauthorized. Missing, malformed, or revoked key.

  • 403

    Forbidden. A send key cannot search. Searching needs a read key or a full key.