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
# 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 -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:
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:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Allowed per minute (always 60 today) |
X-RateLimit-Remaining | Calls left in the current window |
X-RateLimit-Reset | Unix timestamp when the window resets |
Retry-After | Seconds 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:
{
"error": {
"code": "invalid_request",
"message": "`input` is required and must be a non-empty string."
}
}
| Status | code | When |
|---|---|---|
400 | invalid_request | Bad input. message tells you which field. |
401 | unauthorized / invalid_api_key | Missing, malformed, or revoked key. |
404 | not_found | Resource doesn't exist or isn't yours. |
412 | gmail_not_connected | Endpoint needs Gmail/Calendar — connect in the Miles app. |
413 | payload_too_large | Bulk endpoint hit its sync cap (100 contacts, 200 ids). |
429 | rate_limited | Slow down — see Retry-After. |
500 | internal_error | Something on our side. Retry with backoff. |
Ask Miles
Single endpoint that wraps the entire Miles brain. Lets you ask anything in natural language.
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 -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"}'
{
"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.
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 -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
{
"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
}
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.
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.
{ "botId": "abc123" }
// — or —
{ "meetingTitle": "Q2 Pricing Call", "meetingDate": "2026-05-09T14:00:00Z" }
Response status values
processed | Transcript ready — payload includes summary, actionItems, decisions, topics, transcriptId, tasksCreated. |
already_processed | Returns the existing transcriptId — fetch full detail via /v1/meetings/:id. |
bot_active | Bot is still in the meeting. Retry after it ends. |
async_transcribing | Recall.ai is generating the transcript (2-5 min). Retry the same call later. |
no_transcript_yet | Recording exists but transcript not built yet. Retry. |
no_recording | Bot 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.
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 https://api.heymiles.app/api/v1/meetings/last \
-H "Authorization: Bearer $KEY"
List processed meetings, most-recent-first. range is past (default) or today. Returns brief previews + counts.
Full detail for one meeting: rich markdown summary, raw transcript, actionItems, decisions, topics, followUpDraft.
Remove a meeting and its derived records (action items, decisions, follow-up draft, memory entries). Returns { deleted: true }.
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.
Create or update a contact. Upserts by (userId, email). Email is optional when name or company is present.
{
"name": "Sarah Chen",
"email": "sarah@weka.vc",
"company": "Weka Ventures",
"title": "Partner",
"notes": "Met at Demo Day. Lead on early-stage."
}
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.
{ "contacts": [
{ "name": "Sarah Chen", "email": "sarah@weka.vc" },
{ "name": "Bill Barry", "email": "bill@modius.com", "title": "CEO" }
]}
Paginated search. q matches across name / email / company / notes.
Fetch one contact by id.
Tasks
Push items onto Miles's radar. Mirrors to the in-app Actions page automatically.
Create a task. Required: title. Validated horizon: right_now | short_term | horizon. Validated status: pending | in_progress | completed | cancelled.
{
"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."
}
List tasks. Filter by horizon and/or status.
Memory
Stash arbitrary key/value facts that Miles remembers across sessions.
Upsert a memory by (userId, key). Required: category, key, value.
{
"category": "preference",
"key": "preferred_meeting_time",
"value": "Mornings before 11am"
}
Search memory. q matches across key and value.
Delete a memory entry by id.
Decisions
Every decision Miles has captured. Each has topic, decision, context, and the source meeting.
Sprints
Read team sprints with custom kanban stages.
Lists every sprint (active / planned / completed) with task and done counts.
Full detail with stages and tasks. Each task carries its stageId, priority, blocked reason, due date, and assignee.
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.
{ "query": "from:sarah pricing", "limit": 15 }
Briefing
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
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:
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
| Type | Fires 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)
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.