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.
412gmail_not_connectedEndpoint needs Gmail/Calendar — connect in the Miles app.
413payload_too_largeBulk endpoint hit its sync cap (100 contacts, 200 ids).
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. Upsert by email, partial updates supported.

POST /api/v1/contacts

Create or update a contact. Upserts by (userId, email). Email is optional when name or company is present.

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

Sync up to 100 contacts in one call. 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" },
  { "name": "Bill Barry", "email": "bill@modius.com", "title": "CEO" }
]}
GET /api/v1/contacts?q=&company=&limit=&offset=

Paginated search. q matches across name / email / company / notes.

GET /api/v1/contacts/:id

Fetch one contact by id.

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 sprints with custom kanban stages.

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 its stageId, priority, blocked reason, due date, and assignee.

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