Miles API · v1

The Miles API

Ask Miles in natural language. Process meeting transcripts. Sync contacts and tasks. Subscribe to webhook events. Bearer-token auth, JSON in, JSON out — same brain that powers the Miles app, exposed as a REST API.

Quick start

Three minutes from zero to "what was my last meeting about?"

1. Mint an API key

Open Settings → API Keys in the Miles app, click New key, name it after the integration ("Zapier production", "GPT custom action", etc.), and copy the value once — Miles never shows it again.

Keys are prefixed miles_live_. Treat them like passwords — they grant full read/write access to that user's data. Revoke instantly from the same Settings page.

2. Verify connectivity

curl
# Public — no key needed
curl https://api.heymiles.app/api/v1/health

# Authenticated — replace with your key
curl https://api.heymiles.app/api/v1/me \
  -H "Authorization: Bearer miles_live_..."

3. Ask Miles something

curl
curl -X POST https://api.heymiles.app/api/v1/ask \
  -H "Authorization: Bearer miles_live_..." \
  -H "Content-Type: application/json" \
  -d '{"input":"What was my last meeting about?"}'

That's it. Every endpoint below follows the same shape: bearer auth, JSON body, JSON response. No SDK required.

Authentication

All authenticated endpoints expect a bearer token in the Authorization header:

http
Authorization: Bearer miles_live_<32-character-secret>

Tokens are bcrypt-hashed at rest. Miles only knows the prefix (miles_live_a…) for display. Lose the value? Mint a new key and revoke the old one.

Rate limits

60 requests per minute, per key. Every response carries headers so you can self-throttle:

HeaderMeaning
X-RateLimit-LimitAllowed per minute (always 60 today)
X-RateLimit-RemainingCalls left in the current window
X-RateLimit-ResetUnix timestamp when the window resets
Retry-AfterSeconds to wait (only on 429)

Hitting the limit returns HTTP 429. Need more throughput? Mint a second key for the second workload.

Errors

Errors come back as JSON with a stable shape:

json
{
  "error": {
    "code": "invalid_request",
    "message": "`input` is required and must be a non-empty string."
  }
}
StatuscodeWhen
400invalid_requestBad input. message tells you which field.
401unauthorized / invalid_api_keyMissing, malformed, or revoked key.
404not_foundResource doesn't exist or isn't yours.
409conflict / batch_rolled_backThe card changed since your ifUpdatedAt — re-read and retry. Or an atomic batch failed at index; nothing was applied.
412gmail_not_connectedEndpoint needs Gmail/Calendar — connect in the Miles app.
413payload_too_largeBulk endpoint hit its sync cap (100 contacts, 200 ids, 100 batch operations).
429rate_limitedSlow down — see Retry-After.
500internal_errorSomething on our side. Retry with backoff.

Ask Miles

Single endpoint that wraps the entire Miles brain. Lets you ask anything in natural language.

POST /api/v1/ask

Routes the input through Miles's intent parser and the matching handler — calendar, inbox, tasks, memory, transcripts, and more. Returns a natural-language response plus any structured data Miles surfaced.

Request body

input (string, required)The question or instruction.
history (array, optional)Prior turns, each { role, content }, for multi-turn context.
documentId (string, optional)Reference an uploaded document Miles has parsed.
curl
curl -X POST https://api.heymiles.app/api/v1/ask \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{"input":"Find inbox info for my 4pm meeting"}'
json response
{
  "intent": "meeting_prep",
  "response": "From your inbox, three threads relate to your 4pm with Sarah...",
  "data": {
    "meetings": [{ "summary": "Pricing review", "time": "4:00 PM - 4:30 PM" }],
    "emails": [{ "subject": "Re: Pricing draft", "from": "sarah@…", "snippet": "…" }]
  },
  "navigation": "/meetings/<id>/prep",
  "suggestedFollowups": ["Draft a follow-up", "What's after this meeting?"]
}

Transcripts

Drop a meeting transcript in, get a Fireflies-style structured digest back.

POST /api/v1/transcripts

Runs the transcript through Miles's processing pipeline: rich markdown summary (lead + Overview + sectioned notes), action items with priorities, decisions with reasoning, topics, attendees, and a drafted follow-up email. Side-effect: creates Tasks and Action records in the user's app.

meetingTitle (string, required)Display title.
rawTranscript (string, required)The full transcript. Min 20 chars.
meetingDate (ISO string, optional)Defaults to now.
attendees (string[], optional)Names or emails.
externalId (string, optional)Your own id for dedup.
source (string, optional)Defaults to "api".
curl
curl -X POST https://api.heymiles.app/api/v1/transcripts \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "meetingTitle": "Vectris Q2 sync",
    "meetingDate": "2026-05-09T14:00:00Z",
    "attendees": ["Bill Barry", "Vicki Chen"],
    "rawTranscript": "Speaker 1: Lets review pricing..."
  }'

Response

json
{
  "transcriptId": "clx…",
  "summary": "Bill and Vicki aligned on Q2…\n\n**Overview**\n- …",
  "actionItems": [{ "title": "Send pricing deck by EOD Friday", "priority": "immediate" }],
  "decisions": [{ "topic": "Pricing model", "decision": "Per-seat for Q2" }],
  "topics": ["pricing", "Q2 launch"],
  "tasksCreated": 3,
  "memoryRecords": 5,
  "followUpDrafted": true
}
GET /api/v1/transcripts/recordings

List the Recall.ai bot recordings available for grabbing. Each record carries a botId (use it directly with /transcripts/grab for the cheapest match), meeting title/date, current status, and whether it's already been processed.

POST /api/v1/transcripts/grab

Pull a transcript from a Recall.ai bot recording (Miles' own bot) and run it through the processing pipeline. Equivalent to the in-app "Grab Transcript" button, exposed for automation. Either pass botId directly (best) or any of meetingTitle, meetingDate, meetingId and Miles fuzzy-matches a recording.

json body
{ "botId": "abc123" }
// — or —
{ "meetingTitle": "Q2 Pricing Call", "meetingDate": "2026-05-09T14:00:00Z" }

Response status values

processedTranscript ready — payload includes summary, actionItems, decisions, topics, transcriptId, tasksCreated.
already_processedReturns the existing transcriptId — fetch full detail via /v1/meetings/:id.
bot_activeBot is still in the meeting. Retry after it ends.
async_transcribingRecall.ai is generating the transcript (2-5 min). Retry the same call later.
no_transcript_yetRecording exists but transcript not built yet. Retry.
no_recordingBot finished but no recording exists — bot likely failed to join.
no_bot (404)No bot matches the given criteria.

Meetings

Read past meetings — list, single, "the last one", or delete.

GET /api/v1/meetings/last

The single most recent processed meeting with full detail. Powers "what was my last meeting about?" use cases without needing to list-then-fetch.

curl
curl https://api.heymiles.app/api/v1/meetings/last \
  -H "Authorization: Bearer $KEY"
GET /api/v1/meetings?range=past&limit=20

List processed meetings, most-recent-first. range is past (default) or today. Returns brief previews + counts.

GET /api/v1/meetings/:id

Full detail for one meeting: rich markdown summary, raw transcript, actionItems, decisions, topics, followUpDraft.

DELETE /api/v1/meetings/:id

Remove a meeting and its derived records (action items, decisions, follow-up draft, memory entries). Returns { deleted: true }.

POST /api/v1/meetings/bulk-delete

Delete up to 200 meetings in one call. Body: { "ids": [...] }. Returns { deleted, errors, requested }.

Contacts

Sync contacts in and out of Miles's CRM (Memory → People). Upsert by email, partial updates, and free-form tags.

POST /api/v1/contacts

Create or update a contact. Upserts by (userId, email). Email is optional when name or company is present. tags are trimmed + lowercased (max 20) and merged with existing tags by default — send "tagsMode": "replace" to make your payload authoritative. Every response includes tags: string[].

json body
{
  "name": "Sarah Chen",
  "email": "sarah@weka.vc",
  "company": "Weka Ventures",
  "title": "Partner",
  "notes": "Met at Demo Day. Lead on early-stage.",
  "tags": ["investor", "warm"],
  "tagsMode": "merge"
}
POST /api/v1/contacts/bulk

Sync up to 100 contacts in one call — each may carry its own tags; a request-level tagsMode applies to the batch. Returns { created, updated, skipped, errors, total }. Perfect for "new row in Sheets → push to Miles" Zapier flows.

json body
{ "contacts": [
  { "name": "Sarah Chen", "email": "sarah@weka.vc", "tags": ["investor"] },
  { "name": "Bill Barry", "email": "bill@modius.com", "title": "CEO", "tags": ["ais6", "speaker"] }
]}
GET /api/v1/contacts?q=&company=&tag=&limit=&offset=

Paginated search. q matches across name / email / company / notes. tag=investor,warm returns contacts carrying all listed tags.

GET /api/v1/contacts/tags

Every tag in use, with contact counts: { tags: [{ tag, count }], count }.

GET /api/v1/contacts/:id

Fetch one contact by id.

POST /api/v1/contacts/:id/tags

Add tags to a contact (idempotent). Body { "tags": ["speaker"] }. Use PUT on the same path to replace the whole set ([] clears), and DELETE /api/v1/contacts/:id/tags/:tag to remove one. All return the updated contact.

Tasks

Push items onto Miles's radar. Mirrors to the in-app Actions page automatically.

POST /api/v1/tasks

Create a task. Required: title. Validated horizon: right_now | short_term | horizon. Validated status: pending | in_progress | completed | cancelled.

json body
{
  "title": "Send pricing proposal to Sarah",
  "horizon": "right_now",
  "dueDate": "2026-05-12",
  "description": "Per the Vectris call — needs the per-seat tier laid out."
}
GET /api/v1/tasks?horizon=&status=&limit=

List tasks. Filter by horizon and/or status.

Memory

Stash arbitrary key/value facts that Miles remembers across sessions.

POST /api/v1/memory

Upsert a memory by (userId, key). Required: category, key, value.

json body
{
  "category": "preference",
  "key": "preferred_meeting_time",
  "value": "Mornings before 11am"
}
GET /api/v1/memory?category=&q=&limit=

Search memory. q matches across key and value.

DELETE /api/v1/memory/:id

Delete a memory entry by id.

Decisions

GET /api/v1/decisions?since=&meetingId=&limit=

Every decision Miles has captured. Each has topic, decision, context, and the source meeting.

Sprints

Read team sprint boards, and create, move, edit, or delete cards — one at a time or a whole board in one call.

GET /api/v1/sprints?status=&limit=

Lists every sprint (active / planned / completed) with task and done counts.

GET /api/v1/sprints/:id

Full detail with stages and tasks. Each task carries stageId, stageName, status, priority, blocked reason, due date, primary assignee, full assignees roster, category, externalId, and updatedAt. Read stage ids here before writing — stages are addressed by id (names aren't unique).

POST /api/v1/sprints/:id/tasks

Create a card. Defaults to the first stage. Assign by assigneeIds, assigneeEmails (team members only), and/or assigneeAgentIds. Give it an externalId (your CRM row / sheet id — unique per sprint) and a retried create updates that card instead of duplicating it (200 instead of 201). status is derived from the stage. category is an optional free-text board label (max 40 chars) — the in-app board can be filtered down to one category.

json body
{
  "title": "Draft AIS6 sponsor deck",
  "stageId": "3f1c…",
  "priority": "high",
  "dueDate": "2026-09-01",
  "assigneeEmails": ["troy@ignitegtm.com"],
  "category": "Finance",
  "externalId": "crm-42"
}
PATCH /api/v1/sprints/:id/tasks/:taskId

Edit, move, or reassign a card. Any of title, description, stageId, priority, dueDate, blockedReason, category, externalId, assignee keys (sending any assignee key replaces the whole roster; [] unassigns). Moving to a Done-type stage sets status: "done" + completedAt. Add ifUpdatedAt (the card's last updatedAt) to make the write conditional — you get 409 conflict with the card's current state if someone moved it first. DELETE on the same path removes the card.

POST /api/v1/sprints/:id/tasks/batch

Update a whole board in one request: up to 100 ordered create / update / delete operations, each with exactly the granular body above. Give a create a ref and later ops can address that card as "$ref". By default each op commits on its own and failures are isolated per item; "atomic": true makes it all-or-nothing (a failure returns 409 batch_rolled_back with the failing index and cause); "dryRun": true validates everything — refs, stages, assignees, ids — and writes nothing. A malformed op fails the whole batch with 400 + index before anything runs. Only real stage moves emit sprint.stage_changed / task.completed webhooks, after commit. For CSV / CRM sync: put an externalId on every create and re-run the same batch freely — it converges instead of duplicating.

json body
{
  "operations": [
    { "op": "create", "ref": "row-1", "data": { "title": "Book venue", "externalId": "sheet-1" } },
    { "op": "update", "taskId": "$row-1", "data": { "stageId": "<in-progress-stage-id>" } },
    { "op": "update", "taskId": "8f0c…", "data": { "priority": "critical" } },
    { "op": "delete", "taskId": "2b1e…" }
  ],
  "atomic": false,
  "dryRun": false
}
response
{
  "results": [
    { "index": 0, "op": "create", "ref": "row-1", "ok": true, "created": true, "task": { … } },
    { "index": 1, "op": "update", "taskId": "…", "ok": true, "task": { … } },
    { "index": 3, "op": "delete", "taskId": "…", "ok": false, "error": { "code": "not_found", "message": "Task not found" } }
  ],
  "succeeded": 3, "failed": 1, "total": 4, "atomic": false
}
POST /api/v1/sprints/:id/import

Copy an existing Miles Action or Task onto the board as a card. Body { "source": "action" | "task", "sourceId", "stageId"? }. Preserves the assignee (human or agent).

Inbox search

POST /api/v1/inbox/search

Runs a Gmail query against the user's full inbox (no time window). Returns matched threads. Auto-prefixes in:inbox, falls back to recent emails when nothing matches.

Returns 412 if the user hasn't connected Gmail. For an AI-summarized answer instead of raw threads, use /v1/ask.

json body
{ "query": "from:sarah pricing", "limit": 15 }

Briefing

GET /api/v1/briefing/today

Generates a fresh daily briefing via Claude. Returns the structured briefing (greeting, narrative, attentionItems, suggestedActions) plus the events / emails / pending actions it was composed from.

This is an LLM call — costs a few cents per request and takes 2–4s. Cache downstream if you'll call it more than once per day.

Activity

GET /api/v1/activity?since=&action=&limit=

Activity log feed. Common action values: briefing_generated, transcript_processed, task_created, email_sent, meeting_prepped, action_executed.

Webhooks

Get a POST when something happens in Miles, instead of polling.

Webhooks are configured per-user in Settings → Webhooks. Each one has a URL, a per-webhook signing secret (whsec_…), and a list of subscribed event types.

Every delivery is HMAC-SHA256 signed over {timestamp}.{body}. Verify on your receiver before trusting a request — see the example below.

Delivery shape

Every webhook POST has the same envelope:

http
POST /your-receiver
Content-Type: application/json
User-Agent: Miles-Webhook/1.0
X-Miles-Timestamp: 1715299200
X-Miles-Signature: sha256=<hex>

{
  "id": "01HXY…",
  "type": "transcript.processed",
  "createdAt": "2026-05-09T20:00:00Z",
  "data": { /* event-specific payload */ }
}

Event catalog

TypeFires when
transcript.processed A transcript finishes processing — payload includes summary, action items, decisions, topics.
task.created A new task hits the user's radar (API, command bar, transcript, etc.).
task.completed A task moves to completed — including via a sprint stage move.
sprint.stage_changed A task moves between kanban stages on the team Sprints view.

Signature verification (Node.js)

javascript
import crypto from 'crypto';

function verifyMilesWebhook(req, secret) {
  const sigHeader = req.headers['x-miles-signature'];
  const ts = req.headers['x-miles-timestamp'];
  if (!sigHeader || !ts) return false;

  // Reject anything older than 5 minutes (replay protection)
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;

  const presented = sigHeader.split('=')[1];
  const rawBody = req.rawBody; // the unparsed string body — capture before JSON.parse
  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${ts}.${rawBody}`)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(presented, 'hex'),
    Buffer.from(expected, 'hex'),
  );
}

Retries & auto-pause

Failed deliveries (network error or non-2xx) are retried with exponential backoff: 1m, 5m, 30m, 2h, 12h. After 5 attempts a delivery is marked permanently failed. After 5 consecutive failures across deliveries, the webhook is auto-paused and you'll see the failure count in Settings → Webhooks.

Test deliveries

Settings → Webhooks → "Send test" fires a synthetic transcript.processed event so you can validate signature handling without waiting for a real meeting.

Ready to build?

Mint your first key in Settings, then make your first call — the /v1/ask endpoint alone is a one-line integration.

Get your API key