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://braincrumb-api.erikkwilder.workers.dev
Quickstart

Your first capture, in about a minute.

  1. Create a key

    In the web app, under Settings → Connected devices, 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://braincrumb-api.erikkwilder.workers.dev/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 write-only key in Settings → Connected devices.

  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://braincrumb-api.erikkwilder.workers.dev/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. That’s the API.

Every request authenticates with an API key in the Authorization: Bearer bc_sk_… header. Keys are scoped: write-only keys can only create captures, read and full-scope keys can also list and search. A missing or bad key gets a 401; a key without the needed scope gets a 403.

POST/capturesWrite scope

Create a capture. Send the thought as text; if you leave category out, the server classifies it: files it as a contact, event, to-do, idea, note, or link 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. 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”.

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.
201 Created
{
  "id": "5b21c6a5-8f4e-4c9a-9d3e-2f7a1c0b8d44",
  "rawText": "Idea: braincrumb sticker on the coffee machine",
  "category": "idea",
  "confidence": 0.91,
  "isProcessed": true,
  "extractedData": { "title": "BrainCrumb sticker on the coffee machine" },
  "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).

  • 401

    Unauthorized. Missing, malformed, or revoked key.

  • 403

    Forbidden. The key exists but lacks write scope.

  • 429

    Daily cap reached. Free keys get 50 captures a day, reset at midnight UTC. 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.
searchstringSubstring match on the text, transcription, and place name. 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. Write-only keys cannot read. Reading needs a read or full-scope key (Plus).

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. Write-only keys cannot search.