Miles API Cookbook · Verified Aug 2026

Connected APIs cookbook

Miles's Connected APIs feature (Settings → Connected APIs) points Miles at any REST API that authenticates with a static key. Once connected, you can ask Miles for data from that service in Slack (@Miles (Work)) or use it inside agent runs — no code, no Zapier.

The in-app chat box can set up a connection ("connect me to Luma") but does not execute calls; calls happen from Slack and agents.

How to connect anything (2 minutes)

1.Settings → Environment → add a secret (e.g. LUMA_API_KEY) with the service's key. Keys are encrypted and injected server-side — Miles never sees the value.

2.Settings → Connected APIsName, Base URL (the API host + version prefix — not the docs page), Auth type (Bearer / Header / Query / Basic), the secret to use, and Usage notes (paste the block for the service below).

3.Ask Miles in Slack. Reads (GET) run instantly. Writes (POST/PUT/PATCH/DELETE) stage a Confirm card showing the exact request — nothing fires until you click Confirm. Some APIs implement reads as POST (Ashby, Attio, HubSpot search, Notion search) — those also ask for a Confirm; that's expected.

Conventions Miles follows (worth knowing when you write your own notes):

  • Paths in usage notes are relative to the Base URL.
  • Miles's query map sends one value per key. For APIs that want repeated keys (fields[]=A&fields[]=B), put the query string directly in the path (/Table?fields[]=A&fields[]=B).
  • Extra non-auth headers (e.g. Notion-Version) can be added per call — say so in the notes.
  • Responses are capped at 1 MB; prefer paged endpoints for big payloads.
  • For Basic auth where the key is the username, the secret value is <key>: (key, colon, blank password).

The apps

AppCategoryAuth typePlan needed for APIReads instant?
LumaEventsHeader — x-luma-api-keyRequires an active Luma Plus subscription on the calendar you want toYes
StripePayments / BillingBearer tokenAny Stripe account (no paid plan)Yes
GranolaMeeting notesBearer tokenBusiness or Enterprise planYes
PipedriveCRMHeader — x-api-tokenAny paid Pipedrive plan (Lite/Growth/Premium/Ultimate) — every user haYes
FathomMeeting notesHeader — X-Api-KeyIncluded on all Fathom plans (free and paid) per the official FAQYes
HubSpotCRMBearer tokenAny HubSpot tier, including Free CRMYes
NotionDocs / Wiki / Project management (pages + databases)Bearer tokenCreate an internal connection in the Notion Developer portal (https://Mostly (some reads are POST → Confirm)
AirtableDocs / Databases (CRM, pipeline, ATS, ops trackers built in Airtable)Bearer tokenAvailable on ALL Airtable plans (Free includedYes
CalendlySchedulingBearer tokenAny Calendly plan including Free can create a personal access token (FYes
TypeformFormsBearer tokenAny Typeform account can use the APIs (no developer account or paid plYes
beehiivNewsletterBearer tokenAPI access is included on every beehiiv plan including the free LaunchYes
AshbyRecruiting / ATSHTTP BasicAn Ashby Admin creates the key at Admin → Integrations → API KeysNo — all reads are POST → Confirm
AttioCRMBearer tokenAvailable on all Attio plans, but only workspace admins can create tokMostly (some reads are POST → Confirm)
GitHubDevBearer tokenAny GitHub account (Free included) can create a personal access tokenYes

Luma

Events. Lets Miles read your Luma calendar's events, guest lists (RSVPs, approvals, check-ins, registration answers) and calendar-wide contacts, and stage guest adds/approvals from Slack.

Try asking Miles:

  • @Miles pull the guest list for next week's founder dinner on Luma - who's approved, who's still pending, and who's on the waitlist?
  • @Miles who registered for our AI meetup on Luma in the last 24 hours, and what company/role did they put in the registration questions?
  • @Miles how many people actually checked in at Thursday's event vs registered, and drop the no-shows into a Google Sheet.
  • @Miles add jane@acme.com and raj@startup.io as approved guests to the Luma event on Sept 12.

Connect it today

  • Get a key: Requires an active Luma Plus subscription on the calendar you want to access. API keys are created per calendar at luma.com/calendar/manage/api-keys (Calendar -> Settings -> Developer -> API Keys); each key is scoped to exactly one calendar and grants full access to it. Organization-level keys (500 req/min, span all calendars) exist but docs do not say which plan unlocks them.
  • Base URL: https://public-api.luma.com (paths in the notes below include the /v1/ prefix)
  • Auth type: Header — x-luma-api-key — Opaque string; no documented prefix (older community examples show keys beginning with 'secret-', unverified)
  • Secret name (suggested): LUMA_API_KEY
  • Auth note: Raw key in the x-luma-api-key header, no 'Bearer' prefix. Missing key returns 400 'Please provide an API key.'; invalid key returns 401 'You are not signed in.'. Key is scoped to a single calendar. Base URL moved from api.lu.ma/public/v1 to public-api.luma.com with /v1/... paths; the legacy host/paths still respond but are undocumented. Optional x-luma-calendar-id header only matters for organization keys when approving/rejecting calendar submissions.

Good to know

  • Base URL changed: docs now use https://public-api.luma.com with /v1/{resource}/{action} paths (e.g. /v1/events/guests/list). The old api.lu.ma/public/v1/... host and legacy paths (/v1/event/get-guests, /v1/calendar/list-events) still respond but are undocumented; new integrations should use the /v1/... paths.
  • Every write is a POST (create/update/delete/add/approve) - all stage a Confirm card in Miles Slack. Reads are all GET, so nothing read-only will trigger a confirm.
  • The key is scoped to ONE calendar and grants full access to it. Users with several calendars need one Miles connection per calendar (or an org key). /v1/calendars/events/list defaults to events the calendar manages; add access=view to include listed-but-managed-elsewhere events (returned with obfuscated location, no meeting_url).
  • Parameter naming was standardized in April 2026: use event_id (not event_api_id) and read id (not api_id) - old names still accepted but deprecated. Response entries are now flat (guest fields at top level, not nested under guest/event).
  • Event list entries do NOT include description/description_md (removed July 2026); fetch /v1/events/get for the full event. guest_counts (approved/pending_approval/waitlist/invited/declined/checked_in) also only appear on /v1/events/get.
  • Rate limits: Per-minute limit shared across GET and POST: 200 requests/minute per calendar for calendar API keys (and OAuth tokens); 500 requests/minute per organization for organization API keys. Exceeding returns 429 Too Many Requests and blocks for 1 minute. Headers: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, and Retry-After (seconds, on 429). Higher limits via support@luma.com.
Usage notes — paste into the Connected API form
Luma Public API. Auth (x-luma-api-key) is handled by Miles; base URL is set, so use RELATIVE paths. Key is scoped to ONE calendar. Reads are GET; ALL writes are POST. IDs: events evt-..., guests gst-... Times are ISO 8601 UTC. No extra headers.

READS (GET, query params):
- /v1/calendars/events/list ?after=&before=&sort_column=start_at&sort_direction=asc|desc -> {entries:[{id,name,start_at,end_at,timezone,url,registration_open,spots_remaining}],has_more,next_cursor}. No description; use events/get.
- /v1/events/get ?event_id=evt-... -> full event incl. hosts, registration_questions, guest_counts {approved,pending_approval,waitlist,invited,declined,checked_in}.
- /v1/events/guests/list ?event_id=evt-...&approval_status=approved|pending_approval|invited|declined|waitlist&sort_column=registered_at|checked_in_at|name&sort_direction=desc -> entries [{id,user_email,user_name,approval_status,registered_at,utm_source,registration_answers,event_tickets:[{name,amount,checked_in_at}]}]. Checked in = event_tickets[].checked_in_at set.
- /v1/events/guests/get ?event_id=&id=<gst- id or email> -> guest + ticket orders.
- /v1/calendars/contacts/list ?query=<name/email>&tags=<tag>&sort_column=event_approved_count|event_checked_in_count|revenue_usd_cents -> calendar-wide contacts with counts, revenue, tags.

PAGINATION: pagination_limit (server caps it) + pagination_cursor=<next_cursor>; loop while has_more.

WRITES (POST JSON; each stages a Confirm card):
- /v1/events/guests/add {event_id, guests:[{email,name}], approval_status?: approved|pending_approval|waitlist, send_email?} - adds as Going, emails guests.
- /v1/events/guests/update-status {event_id, guest_id (gst- or email), status: approved|declined|pending_approval|waitlist, message?}.

LIMITS: 200 req/min; 429 sends Retry-After.

Unverified against official docs: API key string format/prefix (e.g. 'secret-...') is not shown in official docs. · Maximum value for pagination_limit is not documented ('server will enforce a maximum'). · Which plan unlocks organization-level API keys (500 req/min) - the official docs describe them but do not state plan or where they are created; a third-party review claims Enterprise. · Whether the legacy host api.lu.ma will remain available long-term (it still responds today, but is undocumented).

Sources: [docs.luma.com](https://docs.luma.com/reference)) · [docs.luma.com](https://docs.luma.com/reference/api-conventions.md)) · [docs.luma.com](https://docs.luma.com/reference/rate-limits.md))


Stripe

Payments / Billing. Lets Miles answer revenue, customer, subscription, invoice and payout questions straight from your Stripe account (and, if you grant Write, tag customers or send invoices) from Slack.

Try asking Miles:

  • @Miles how much did we collect in Stripe this week, and what were the fees?
  • @Miles which invoices are open and past due right now, with the customer email and amount?
  • @Miles who churned this month - list the subscriptions canceled since the 1st with the customer name and plan.
  • @Miles do we have a Stripe customer for anyone at acme.com, and are they on an active subscription?

Connect it today

  • Get a key: Any Stripe account (no paid plan). Create the key in the Stripe Dashboard: Developers > API keys (dashboard.stripe.com/apikeys) > "Create restricted key" > name it (e.g. miles-readonly) > set Read on the resources Miles should see (Customers, Subscriptions, Invoices, Charges/PaymentIntents, Balance, Balance transactions) and None on everything else > Create key > complete the two-factor prompt > copy the rk_live_... value (shown once). Toggle sandbox/live mode on that page: the key you paste determines whether Miles sees test or real data. Accounts created before May 2026 may have no restricted keys yet; create one rather than pasting the sk_live_ secret key.
  • Base URL: https://api.stripe.com/v1
  • Auth type: Bearer token — rk_live_… (restricted, recommended) / rk_test_… (sandbox); sk_live_… / sk_test_… also work but are unrestricted
  • Secret name (suggested): STRIPE_API_KEY
  • Auth note: Stripe's canonical auth is HTTP Basic with the key as username and empty password, but Authorization: Bearer <key> is officially supported and is the simplest fit for Miles (verified live). If you use Miles' basic type instead, the vault value must be "rk_live_xxx:" (key, colon, empty password). Optional Stripe-Version header pins an API version; not required (defaults to the account's version).

Good to know

  • Base URL for Miles is https://api.stripe.com/v1 - paths must be relative WITHOUT /v1 (use /customers, not /v1/customers) or the request will 404.
  • Stripe only accepts form-encoded request bodies (application/x-www-form-urlencoded) and rejects JSON with 'Invalid request (check that your POST content type...)' - verified live. Miles' call_api JSON-encodes object bodies, so for any write the model must pass body as a pre-encoded string (a=b&metadata[k]=v) and set header content-type: application/x-www-form-urlencoded.
  • Nested filters are flat bracket keys in the query string: created[gte]=1720000000, not a JSON object. Miles' query map stringifies values, so always use the bracket key form; expand[] works the same way (one value per key).
  • All amounts are integers in the smallest currency unit (1099 = $10.99) and all timestamps are Unix seconds - convert before answering.
  • The key decides the mode: rk_test_/sk_test_ keys only see sandbox data, rk_live_/sk_live_ only see real data. Paste the live restricted key for real answers.
  • Rate limits: Global 100 requests/sec per account in live mode, 25/sec in sandbox; individual endpoints 25/sec unless noted; Search API 20 read req/sec; Files 20/sec. Exceeding returns HTTP 429 with a Stripe-Rate-Limited-Reason header (global-rate, endpoint-rate, global-concurrency, endpoint-concurrency, resource-specific); 429 with code lock_timeout is contention, not rate limiting - retry with backoff. Separate read allocation: GET requests must average <=500 per transaction over a rolling 30 days, with a floor of 10,000 reads/month for every account.
Usage notes — paste into the Connected API form
Auth (Bearer, restricted READ-ONLY key) is injected; base URL is https://api.stripe.com/v1 so paths are RELATIVE (/customers, never /v1/customers). Amounts are integers in smallest currency unit (1099 = $10.99); timestamps are Unix seconds. Lists take limit (1-100, default 10), starting_after=<last id> to page (stop when has_more=false), and flat keys created[gte] / created[lte].
READS (GET, run now):
- /customers?email=<exact>&created[gte]=<ts> - newest first.
- /customers/search?query=email~"acme.com" - fuzzy; also name~"Jane", metadata["k"]:"v"; page with page=<next_page>. Lags ~1 min.
- /subscriptions?status=active|trialing|past_due|canceled|all&customer=cus_x&price=price_x - default EXCLUDES canceled. expand[]=data.customer inlines customer name/email. Churn: /subscriptions/search?query=status:"canceled" AND canceled_at>ts
- /invoices?status=open|paid|uncollectible|void&customer=cus_x&subscription=sub_x - amount_due, amount_paid, due_date, hosted_invoice_url, customer_email.
- /charges?created[gte]=<ts>&customer=cus_x - payments (amount, status, refunded).
- /balance_transactions?type=charge&created[gte]=<ts> - amount, fee, net; /balance = available vs pending.
- /customers/{id}, /subscriptions/{id}, /invoices/{id} for one object.
WRITES (POST - stage a Confirm card; key ALSO needs Write permission, which the read-only key lacks by design): Stripe REJECTS JSON bodies. Send body as a form-encoded STRING with header content-type: application/x-www-form-urlencoded, e.g. "metadata[owner]=jane&description=Enterprise lead" to POST /customers/{id}. POST /invoices/{id}/send (no body) emails an open send_invoice invoice.
Limits: 100 req/s live (25 sandbox), 20 req/s on /search; 429 when over. Read allocation ~500 GETs/transaction (min 10k/mo): filter, keep limit small.

Unverified against official docs: Exact permission-group labels in the Dashboard's Create-restricted-key form (e.g. whether Balance and Balance transactions sit under a 'Reporting' group) - the docs confirm None/Read/Write per resource but not the UI group names. · Whether Miles' 1MB response cap is ever hit at limit=100 with expand[]=data.customer on subscriptions/charges - plausible for large accounts, not measured.

Sources: [docs.stripe.com](https://docs.stripe.com/api)) · [docs.stripe.com](https://docs.stripe.com/keys)) · [docs.stripe.com](https://docs.stripe.com/api/pagination))


Granola

Meeting notes. Lets Miles read your Granola AI meeting notes, attendees, and full transcripts so it can summarize calls, extract decisions and action items, and draft follow-ups from what was actually said.

Try asking Miles:

  • @Miles pull my Granola notes from this week and give me the key decisions and action items from each meeting
  • @Miles find yesterday's Granola call with Acme, summarize it, and draft a follow-up email to the attendees
  • @Miles go through the Granola notes in the 'Candidate Interviews' folder from the last 30 days and give me a strengths/concerns table per candidate
  • @Miles get the full transcript of my Granola meeting with Sarah and tell me exactly what she said about pricing and timeline

Connect it today

  • Get a key: Business or Enterprise plan. Personal keys: any workspace member on Business/Enterprise creates one in the Granola desktop app at Settings → Connectors → API keys → Create new key (choose scopes: Personal notes and/or Public notes; Enterprise admins can restrict scopes under Settings → Workspace → General → API access for members). Workspace API keys (admin-only, non-expiring, cover public notes + spaces with API access enabled): Settings → Connectors → Workspace API keys → Create new key. Keys are prefixed grn_; revocation is permanent.
  • Base URL: https://public-api.granola.ai/v1
  • Auth type: Bearer token — grn_…
  • Secret name (suggested): GRANOLA_API_KEY
  • Auth note: Standard 'Authorization: Bearer grn_...' header; OpenAPI security scheme is HTTP bearer (bearerFormat apiKey). No other required headers. Personal keys are scoped (Personal notes / Public notes) at creation; workspace keys are admin-created and don't expire.

Good to know

  • Business/Enterprise plans only — free/individual Granola accounts cannot create API keys.
  • The API only returns notes that already have a generated AI summary AND transcript; meetings still processing (or notes without a transcript) are absent from /notes and 404 on direct fetch.
  • Key scope matters: a personal key sees only the scopes it was created with (Personal notes = owned/directly shared/private folders shared with you; Public notes = workspace-visible + Team space). Enterprise admins can restrict scopes per member and per space, so 'missing' notes are usually a scope issue.
  • GET /notes returns summaries only (id/title/owner/dates) — Miles must call GET /notes/{id} per note to get content; with a 5 rps limit, keep page_size small and pace loops.
  • No search endpoint: filtering is by created/updated date and folder_id only. To find 'the Acme call', list by date range and match on title/attendees.
  • Rate limits: Per API key: burst of 25 requests within 5 seconds, sustained 5 requests/second (300/minute). Exceeding returns 429 Too Many Requests.
Usage notes — paste into the Connected API form
Granola meeting notes + transcripts. Auth (Bearer grn_ key) is injected by Miles; base URL is https://public-api.granola.ai/v1, so paths are RELATIVE. No extra headers.

READ (run immediately):
- GET /notes — list. Query: created_after, created_before, updated_after (ISO 8601 UTC, e.g. 2026-08-10T00:00:00Z), folder_id (fol_...), page_size (1-30, default 10), cursor. Returns notes[] {id (not_...), title, owner{name,email}, created_at, updated_at}, hasMore, cursor. No content here — call Get Note per note.
- GET /notes/{note_id} — full note: title, owner, web_url, calendar_event {event_title, invitees, organiser, scheduled_start_time, scheduled_end_time}, attendees[{name,email}], folder_membership[], summary_text, summary_markdown (the AI notes). ?include=transcript adds it inline; on 413 TRANSCRIPT_TOO_LARGE use the transcript endpoint.
- GET /notes/{note_id}/transcript — paged transcript[] {speaker{name, attribution me/them}, text, start_time, end_time}, hasMore, cursor. page_size 1-100 (default 50). Prefer for long meetings.
- GET /folders — folders[] {id, name, parent_folder_id}, page_size 1-30, cursor. Resolve folder name -> id.

Pagination: cursor-based — if hasMore is true, repeat the call with ?cursor=<cursor>.

Rate limits: 25-request burst, 5 req/s sustained (300/min); 429 if exceeded — pace loops.

Gotchas: only notes with a finished AI summary AND transcript are returned (in-progress meetings 404). No keyword search — filter by date/folder, then read. Key scope (personal vs public) limits visibility. Transcripts may be missing if auto-deleted.

WRITES (Confirm card; notes cannot be created/edited): POST /webhook-endpoints {url, scopes:["personal"|"public"|"workspace"], events?, folder_ids?} creates a webhook. DELETE /webhook-endpoints/{id} removes one.

Unverified against official docs: Default sort order of GET /notes (docs example implies newest first but never states it). · Whether personal API keys expire (docs only say workspace keys 'don't expire'). · GET /v1/audit endpoint: present in openapi.json but has no documentation page; access requirements (workspace key / admin / Enterprise) unknown. · Exact inline size threshold that triggers 413 TRANSCRIPT_TOO_LARGE is not published. · Rate limits stated per API key in the help center; whether they are additionally per-workspace is not stated.

Sources: [docs.granola.ai](https://docs.granola.ai/introduction)) · [docs.granola.ai](https://docs.granola.ai/api-reference)) · [docs.granola.ai](https://docs.granola.ai/api-reference/list-notes.md))


Pipedrive

CRM. Lets Miles read and update your Pipedrive CRM from Slack — open deals by pipeline/stage/owner, contacts, upcoming activities, notes and pipeline totals — and log calls, notes or stage changes after a Confirm.

Try asking Miles:

  • @Miles which open deals in our Enterprise pipeline haven't been updated in 14 days, and who owns them?
  • @Miles pull everything on the Acme Corp deal — stage, value, recent activities and notes — before my 2pm call
  • @Miles what's our total open pipeline value by stage right now, and what's the weighted number?
  • @Miles log a call activity on the Globex deal for tomorrow at 10am — 'Follow up on pricing' — and add a note that they want a Q4 start

Connect it today

  • Get a key: Any paid Pipedrive plan (Lite/Growth/Premium/Ultimate) — every user has a personal API token by default, no developer app or OAuth needed. Create/copy it at account name (top right) → Company settings → Personal preferences → API (direct link https://app.pipedrive.com/settings/api), "Generate new token". The token is per-user-per-company, only one is active at a time (regenerating it breaks the Miles connection), and it sees exactly what that user can see. If the API tab is missing, an admin must enable it: Settings → Manage Users → Permission sets → the user's set → enable "Access their own API token"/"use API".
  • Base URL: https://<yourcompany>.pipedrive.com/api — replace <yourcompany> with the subdomain you see when logged in (or company_domain from GET /v1/users/me)
  • Auth type: Header — x-api-token — 40-char lowercase hex string (e.g. 659c9fdd…2e03), no prefix
  • Secret name (suggested): PIPEDRIVE_API_KEY
  • Auth note: Raw token in the x-api-token header (no 'Bearer'). Official docs now say the token 'must be provided in the x-api-token header for all requests'; the legacy ?api_token= query param is no longer documented, so use Miles 'header' auth, not 'query'. Content-Type: application/json is needed on writes — Miles adds it automatically for JSON bodies. base ends in /api (not /api/v2) so both /v2/... and /v1/... paths are reachable.

Good to know

  • Two API versions live side by side, so the base URL must end in /api (not /api/v2). Core v1 endpoints (GET /v1/deals, /v1/persons, /v1/organizations, /v1/activities, /v1/pipelines, /v1/stages, /v1/products, /v1/itemSearch and their /{id} and /search variants) were deprecated, removed from the v1 OpenAPI spec, and declared out of support effective Aug 1 2026 — use /v2 for those. But notes, users, leads list, filters, mailbox, deals/summary, deals/timeline, deals/{id}/flow, deals/{id}/participants and activityTypes still exist ONLY in /v1.
  • v2 responses contain IDs only — no embedded person/org/stage/owner names ('related objects have been removed'). To produce a readable answer Miles must also fetch /v2/stages (+/v2/pipelines) and /v1/users once, and /v2/persons/{id} or /v2/organizations/{id} per deal — or use /v2/deals/search which does return stage/person/org names.
  • The company subdomain must be substituted into the base URL (https://acme.pipedrive.com/api). It is the first part of the URL when logged in, or the company_domain field from GET /v1/users/me. Miles does not follow redirects, so a wrong host/http URL will fail rather than bounce.
  • Auth is a personal token: Miles sees and edits with that user's permissions and visibility settings, and audit trails show that user as the actor. Only one active token per user — regenerating it in Pipedrive silently breaks the connection until the secret is updated in Miles.
  • Admins can switch API access off per permission set ('Access their own API token' / 'use API' under Settings → Manage Users → Permission sets); if the API tab is missing, that is why.
  • Rate limits: Two layers. (1) Daily token budget, shared by the whole company (all users + all integrations): 30,000 base tokens × plan multiplier (Lite 1, Growth 2, Premium 5, Ultimate 7) × seats, plus purchased top-ups; resets at midnight server time. Per-call cost is shown in the API reference — v2: get single=1, list=10, search=20, create/update=5, delete=3; v1 is ~2x (list=20, search=40, /deals/summary=40, /deals/{id}/flow=40). When exhausted every call returns 429 until reset; admins get emails at 75%/100%. (2) Burst limit per API token over a rolling 2-second window: Lite 20, Growth 40, Premium 100, Ultimate 120 requests; all Search endpoints 10 req/2s regardless of plan. Headers x-ratelimit-limit / x-ratelimit-remaining / x-ratelimit-reset (and x-daily-requests-left for POST/PUT). Ignoring 429s at high volume gets the token a Cloudflare 403 block.
Usage notes — paste into the Connected API form
Pipedrive CRM. Auth is handled (x-api-token header). Base URL is https://<company>.pipedrive.com/api, so paths are RELATIVE and MUST start with /v2/ or /v1/. Prefer v2. JSON bodies only.

READS (GET, run now):
- /v2/deals?status=open&owner_id=&pipeline_id=&stage_id=&updated_since=<RFC3339>&sort_by=update_time&sort_direction=desc&limit=100. v2 embeds NO names: map stage_id via /v2/stages, owner_id via /v1/users, person/org via /v2/persons/{id}, /v2/organizations/{id}.
- /v2/deals/{id}; /v2/deals/search?term=&status= (has stage/person/org names)
- /v2/persons?owner_id=&org_id=&deal_id=; /v2/persons/search?term=&fields=email,name
- /v2/activities?deal_id=&person_id=&owner_id=&done=false&sort_by=due_date
- /v2/pipelines; /v2/stages?pipeline_id=; /v1/users; /v1/users/me
- /v1/notes?deal_id=&person_id=; /v1/leads?owner_id=; /v1/deals/summary?status=open; /v1/deals/{id}/flow

PAGING: v2 = cursor: limit (max 500), cursor=additional_data.next_cursor until null. v1 = offset start&limit until additional_data.pagination.more_items_in_collection=false. Envelope {success,data,additional_data}. Custom fields under custom_fields keyed by 40-char hash (labels via /v2/dealFields).

WRITES (stage a Confirm card): POST /v2/activities {subject,type:call|meeting|task|email,due_date:"YYYY-MM-DD",due_time:"HH:MM:SS",deal_id|person_id|org_id,note}; POST /v1/notes {content(HTML),deal_id|person_id|org_id}; PATCH /v2/deals/{id} {stage_id,status:"won"|"lost",lost_reason,value,expected_close_date,owner_id}; POST /v2/deals {title req,value,person_id,org_id,stage_id}. v2 = PATCH, never PUT.

LIMITS: shared daily token budget + burst 20-120 req/2s by plan; on 429 back off. Never call v1 /deals,/persons,/activities,/organizations,/pipelines,/stages,/itemSearch (out of support Aug 2026) - use /v2.

Unverified against official docs: Whether the out-of-support v1 core endpoints (GET /v1/deals etc.) still physically respond as of Aug 2026 — Pipedrive says they 'may remain functional' but are not guaranteed; treat as gone. · Whether the legacy ?api_token= query parameter still works on /v2 endpoints — docs no longer mention it (header 'must' be used); not needed since Miles uses the header. · https://api.pipedrive.com/api as an alternative base (v2 OpenAPI server is https://api.pipedrive.com/api/v2 and v1 examples use https://api.pipedrive.com/v1; the migration guide says the /api/v1 prefix was also accepted) — company-domain form is what the docs instruct and is the one recommended here. · Could not live-test any call (no token); endpoint paths/params/costs come from the official OpenAPI specs downloaded 2026-08-17. · Plan availability: Pipedrive KB says all users in any company account can access the API by default; I did not find an official statement excluding any specific paid plan.

Sources: [pipedrive.readme.io](https://pipedrive.readme.io/docs/core-api-concepts-authentication)) · [pipedrive.readme.io](https://pipedrive.readme.io/docs/enabling-api-for-company-users)) · [pipedrive.readme.io](https://pipedrive.readme.io/docs/core-api-concepts-requests))


Fathom

Meeting notes. Lets Miles pull your Fathom-recorded meetings, AI summaries, action items, and full transcripts (filtered by customer domain, team, date, or meeting type) straight into Slack and agent runs.

Try asking Miles:

  • @Miles pull the Fathom summary and action items from our call with acme.com yesterday and post them in #sales
  • @Miles list every external customer call the Sales team recorded in Fathom this week with a one-line takeaway each
  • @Miles grab the transcript for yesterday's Fathom call with Acme and pull out every objection about pricing
  • @Miles which action items from my Fathom calls in the last 7 days are assigned to Sarah and still open?

Connect it today

  • Get a key: Included on all Fathom plans (free and paid) per the official FAQ; no developer program or approval needed. Each user creates their own key at fathom.video User Settings > API Access (https://fathom.video/customize#api-access-header). Keys are per-user, not org-wide: a key sees only meetings that user recorded or that were shared with them/their team, and admin keys do not unlock other users' private meetings. Only GET /users requires an account_admin key.
  • Base URL: https://api.fathom.ai/external/v1
  • Auth type: Header — X-Api-Key — Not documented (opaque string generated in Fathom settings)
  • Secret name (suggested): FATHOM_API_KEY
  • Auth note: Raw key in the X-Api-Key header, no 'Bearer' prefix. The OpenAPI spec also lists an Authorization: Bearer scheme (used by OAuth tokens); whether a plain API key also works as a Bearer token is not confirmed, so use header type.

Good to know

  • Keys are per-user, not per-org: Miles will only see meetings the key owner recorded or that were shared with them/their team. Admin keys do NOT expose other users' unshared/private meetings; if the org wants Miles to see all shared team calls, use a key from an admin who has org-wide view access to shared calls.
  • GET /users returns 403 unless the key owner has settings_access=account_admin.
  • Array filters use bracket names (recorded_by[]=…, teams[]=…, calendar_invitees_domains[]=…). Miles sends one value per query key — for several values, put the query string in the path (e.g. /meetings?recorded_by[]=a@x.com&recorded_by[]=b@x.com). calendar_invitees_domains[] is exact-match; each meeting maps to one company.
  • If recorded_by[] and teams[] are both omitted on /meetings, calls recorded by users outside your Fathom org (e.g. a customer who shared a call with you) are excluded from results.
  • The summary/transcript GETs accept an optional destination_url query param that flips them into async mode (they POST results to that URL and return only {destination_url}). Miles should never send it.
  • Rate limits: Global: 60 calls per 60-second window per user across all of that user's API keys (higher limits not available). Heavy endpoints (/recordings/{id}/summary, /recordings/{id}/transcript, and /meetings with include_summary or include_transcript) are capped at 30 per 60s and may be cut to 5 during high load. Recording downloads: separate 30 per 60s (status polling counts against the global limit). 429 on breach with RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset and Retry-After headers.
Usage notes — paste into the Connected API form
Fathom AI notetaker. Auth (X-Api-Key) is injected; base URL is https://api.fathom.ai/external/v1 so paths are RELATIVE. Key is user-scoped: sees only meetings the owner recorded or that were shared with them/their team.

READS (GET, run immediately):
- /meetings - 10/page; items include recording_id, title, share_url, calendar_invitees[], recorded_by. Filters: created_after / created_before (ISO 8601 UTC), calendar_invitees_domains[]=acme.com (exact; one value per query key — for several, put the query string in path), calendar_invitees_domains_type=only_internal|one_or_more_external, meeting_type, recorded_by[]=<email>, teams[]=<team>. Add include_summary / include_action_items / include_highlights / include_crm_matches / include_transcript=true as needed. Omitting recorded_by[]/teams[] hides outside-org users' calls.
- /recordings/{recording_id}/summary -> {summary:{template_name, markdown_formatted}}. Never pass destination_url (goes async).
- /recordings/{recording_id}/transcript -> [{speaker:{display_name, matched_calendar_invitee_email}, text, timestamp HH:MM:SS}]. Same rule.
- /teams, /team_members?team=<name>, /meeting_types. /users is admin-key only (403).

PAGINATION: responses include next_cursor; re-call with ?cursor=<next_cursor> until null.

RATE LIMITS: 60/60s per user; summary/transcript (and /meetings with include_summary/include_transcript) 30/60s; on 429 wait Retry-After. Prefer per-recording calls to include_transcript on lists (1MB cap).

WRITES (Confirm card): POST /webhooks {destination_url, triggered_for:[my_recordings|shared_team_recordings|...], include_summary/action_items/transcript/crm_matches (>=1 true)} -> id + secret (save id; no list endpoint). DELETE /webhooks/{id}. POST /recordings/{recording_id}/download (202) then GET /recordings/{recording_id}/downloads/{download_id} -> signed URL.

Unverified against official docs: API key string format/prefix is not documented anywhere official. · Whether a plain API key is accepted via Authorization: Bearer (the OpenAPI spec lists a BearerAuth scheme, likely for OAuth tokens); X-Api-Key header is the documented path. · Whether Fathom admins can restrict or disable API-key creation for members (help center says anyone can build; FAQ says admins cannot revoke others' keys). · Whether a request-side page-size/limit parameter exists on /meetings (only the 10-per-page default and a response 'limit' field are documented). · Exact error status returned for a summary/transcript that is still processing (docs list only 400/401/429).

Sources: [developers.fathom.ai](https://developers.fathom.ai)) · [developers.fathom.ai](https://developers.fathom.ai/quickstart.md)) · [developers.fathom.ai](https://developers.fathom.ai/faq.md))


HubSpot

CRM. Lets Miles read your HubSpot CRM (contacts, companies, deals, pipeline stages, owners) and log notes or update deals from Slack, without leaving the conversation.

Try asking Miles:

  • @Miles look up jane@acme.com in HubSpot — what lifecycle stage is she in and who owns her?
  • @Miles list our open deals with amount, stage name and close date, grouped by owner
  • @Miles which HubSpot contacts were created in the last 7 days? Give me name, company and job title
  • @Miles log a note on the Acme deal in HubSpot summarizing today's call and move it to Contract Sent

Connect it today

  • Get a key: Any HubSpot tier, including Free CRM. You must be a Super Admin to create the credential (Service Keys also allow users with the "Developer tools access" permission). Recommended: a Service Key (HubSpot's credential for system-to-system access): Development → Keys → Service keys → Create service key → add scopes (crm.objects.contacts.read, crm.objects.companies.read, crm.objects.deals.read, crm.objects.owners.read, plus .write scopes for notes/deal updates). Fallback: a legacy private app (Development → Legacy apps → Create → Private → Scopes → Auth tab → Show token). Both are static Bearer tokens that don't expire unless rotated.
  • Base URL: https://api.hubapi.com
  • Auth type: Bearer token — pat-na1-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx (region prefix varies, e.g. pat-eu1-…); Service Keys use the same pat- shape
  • Secret name (suggested): HUBSPOT_API_KEY
  • Auth note: Plain static Bearer token (private app access token or Service Key); no OAuth dance, no expiry unless rotated. Every endpoint checks scopes: a token missing crm.objects.deals.read etc. returns 403 MISSING_SCOPES, so add all needed scopes when creating the app/key. POST/PATCH bodies must be JSON (Content-Type: application/json).

Good to know

  • CRM Search (POST /crm/v3/objects/{contacts|companies|deals}/search) is a read that Miles will stage as a Confirm card because it's a POST. It is the only way to filter/sort (by owner, stage, created date, free-text). Mitigations: look up contacts by email with GET /crm/v3/objects/contacts/{email}?idProperty=email, and use GET list + pipelines/owners for small portals; teach users that 'search' asks for a confirm.
  • GET list endpoints have no filtering or sorting and page in record-ID order, max 100 per page — 'newest contacts this week' realistically requires the search POST.
  • Only a minimal default property set is returned unless you pass properties=; jobtitle, lifecyclestage, hubspot_owner_id, amount etc. will be missing otherwise. Keep the properties list tight to stay under Miles' 1MB response cap.
  • dealstage and pipeline come back as internal IDs (e.g. 'appointmentscheduled' or numeric IDs for custom pipelines); hubspot_owner_id is an owner ID, not a user ID or name. Resolve via GET /crm/v3/pipelines/deals and GET /crm/v3/owners before presenting to the user.
  • Search date filters use Unix millisecond timestamps as strings (BETWEEN uses value + highValue); max 200/page, 10,000 results per query, 5 req/s per account, single sort rule, and newly created/updated records may lag a few seconds before appearing.
  • Rate limits: Per private app / service key: 100 requests per 10 seconds (Free/Starter), 190 per 10 s (Professional/Enterprise), 250 per 10 s with the API Limit Increase add-on. Daily cap shared across all apps in the account: 250,000 (Free/Starter), 625,000 (Professional), 1,000,000 (Enterprise). Search endpoints are separately limited to 5 requests per second per account and do not return rate-limit headers. Exceeding limits returns HTTP 429; check X-HubSpot-RateLimit-Max / -Remaining / -Interval-Milliseconds / -Daily / -Daily-Remaining headers.
Usage notes — paste into the Connected API form
Auth (Bearer) is injected by Miles; base URL is https://api.hubapi.com, so paths are RELATIVE. JSON bodies on POST/PATCH.

READS (GET, run immediately):
- /crm/v3/objects/contacts?limit=100&properties=email,firstname,lastname,company,jobtitle,lifecyclestage,hubspot_owner_id,createdate
- /crm/v3/objects/contacts/{email}?idProperty=email (lookup by email, no search)
- /crm/v3/objects/companies?limit=100&properties=name,domain,industry,hubspot_owner_id
- /crm/v3/objects/deals?limit=100&properties=dealname,amount,dealstage,pipeline,closedate,hubspot_owner_id&associations=contacts
- /crm/v3/pipelines/deals -> stage IDs+labels (dealstage/pipeline are internal IDs)
- /crm/v3/owners -> map hubspot_owner_id to name/email
Always pass properties= (defaults are minimal). Lists cannot filter/sort.

SEARCH (POST = Confirm card): POST /crm/v3/objects/{contacts|companies|deals}/search
{"filterGroups":[{"filters":[{"propertyName":"createdate","operator":"GTE","value":"<unix ms>"}]}],"properties":["..."],"sorts":[{"propertyName":"createdate","direction":"DESCENDING"}],"limit":200}
Operators: EQ NEQ LT LTE GT GTE BETWEEN IN NOT_IN HAS_PROPERTY CONTAINS_TOKEN; "query"=free text. Max 200/page, 10k total, 5 req/s.

PAGINATION: paging.next.after -> resend as ?after= (or "after" in search body) until absent.

WRITES (Confirm card):
- POST /crm/v3/objects/notes {"properties":{"hs_timestamp":"<ISO 8601>","hs_note_body":"..."},"associations":[{"to":{"id":"<id>"},"types":[{"associationCategory":"HUBSPOT_DEFINED","associationTypeId":202}]}]} (202=contact,190=company,214=deal)
- PATCH /crm/v3/objects/deals/{dealId} {"properties":{"dealstage":"<stageId>","amount":"12000"}}

LIMITS: 100-190 req/10s (429 if exceeded). Scopes: crm.objects.{contacts,companies,deals}.read/.write, crm.objects.owners.read.

Unverified against official docs: Token prefix pat-na1-/pat-eu1-: shown as a masked example in the Service Keys doc and widely reported in HubSpot Community posts, but the legacy private-apps doc itself does not state the prefix. · Whether the older UI path Settings → Integrations → Private Apps still appears in some accounts; current docs only describe Development → Legacy apps and Development → Keys → Service keys. · Exact default property set returned when properties= is omitted (stated as 'minimal' from experience: e.g. createdate, email, firstname, lastname, hs_object_id, lastmodifieddate for contacts). · Whether search date filters accept ISO 8601 strings in addition to Unix-ms timestamps (docs only show ms). · Whether creating a note associated to a deal/company additionally requires crm.objects.deals.write / crm.objects.companies.write (notes doc lists only crm.objects.contacts.read/write). · API Limit Increase add-on burst limit: usage-guidelines page says 250/app/10s while the private-apps overview says 200 — HubSpot's own docs disagree. · The pipelines doc page mentions crm.pipelines.orders.read scopes; in practice deal pipeline reads work with crm.objects.deals.read but this pairing was not explicitly confirmed.

Sources: [developers.hubspot.com](https://developers.hubspot.com/docs/api/overview)) · [developers.hubspot.com](https://developers.hubspot.com/changelog/introducing-date-based-api-versioning)) · [developers.hubspot.com](https://developers.hubspot.com/docs/api/crm/contacts))


Notion

Docs / Wiki / Project management (pages + databases). Lets Miles search your Notion workspace, read pages as Markdown, pull rows out of Notion databases (roadmaps, CRMs, hiring pipelines, meeting notes), and create or update pages/rows on request.

Try asking Miles:

  • @Miles search our Notion for the 'Q3 GTM plan' page and give me a 5-bullet summary of what's in it
  • @Miles in the Notion Hiring Pipeline database, list every candidate whose Status is 'Onsite' or 'Offer' and who owns each one
  • @Miles what changed in our Notion Product Roadmap in the past week? Show tasks moved to Done and anything now overdue
  • @Miles create a page under 'Meeting Notes' in Notion titled 'Acme discovery call 8/17' with these notes, and set the Status of the Acme row in the Deals database to 'Proposal'

Connect it today

  • Get a key: Create an internal connection in the Notion Developer portal (https://app.notion.com/developers/connections, also reachable via Settings > Connections > "Develop your own connections"). You must be a Workspace Owner to create one; docs do not restrict it by plan (works on standard workspaces; PATs are restricted by default on Business/Enterprise until an owner enables them). Copy the "Installation access token" (a.k.a. internal integration secret) from the connection's Configuration tab and store it in Miles Settings > Environment (e.g. NOTION_API_KEY). Then, in Notion, open each page/database you want Miles to see > ••• menu > Connections > add the connection (child pages inherit). A new connection has NO page access until you do this. Set capabilities (Read/Update/Insert content, optionally Read/Insert comments and user info) in the Developer portal.
  • Base URL: https://api.notion.com/v1
  • Auth type: Bearer token — ntn_… (tokens issued before Sept 25 2024 start with secret_; both still valid)
  • Secret name (suggested): NOTION_API_KEY
  • Extra required header(s): Notion-Version: 2026-03-11
  • Auth note: Standard Bearer. Notion-Version is mandatory on EVERY request; the value pins API semantics. Use 2026-03-11 (latest) so data_sources endpoints and GET /pages/{id}/markdown work; older values like 2022-06-28 change endpoint shapes (databases/{id}/query, archived instead of in_trash, search filter value 'database' instead of 'data_source'). POST/PATCH also need Content-Type: application/json.

Good to know

  • Notion-Version header is REQUIRED on every call; put it in the usage notes and make sure Miles adds it as an extra header. Use 2026-03-11 (latest) consistently — mixing versions changes endpoint shapes.
  • The two most important reads (POST /search and POST /data_sources/{id}/query) are POSTs, so Miles will stage a Confirm card in Slack for them; only /databases/{id}, /data_sources/{id}, /pages/{id}, /pages/{id}/markdown, /blocks/{id}/children, /users are true GETs.
  • Since API version 2025-09-03, databases are containers of data sources: rows are queried via /data_sources/{data_source_id}/query, NOT /databases/{database_id}/query. Get the data_source_id from GET /databases/{database_id} (data_sources[]) or from the Notion UI ('Manage data sources' > 'Copy data source ID').
  • The connection sees nothing by default: each page/database (or an ancestor) must be shared via ••• > Connections. Unshared content returns 404 (not 403), which looks like 'not found'.
  • Under 2026-03-11 the field is in_trash (renamed from archived) and Append Block Children uses a position object instead of after.
  • Rate limits: Average 3 requests/second per connection (short bursts tolerated) plus a workspace-wide limit scaled to plan; exceeding returns HTTP 429 with error code rate_limited and a Retry-After header (integer seconds). 529 = overloaded, retry like 429. Size limits: 500KB and 1,000 blocks per request; 100 elements per block/children array; rich_text 2,000 chars; URLs 2,000 chars; 100 multi-select options / relations / people per property write.
Usage notes — paste into the Connected API form
Notion REST API. Auth (Bearer) is injected and base URL already ends in /v1, so use RELATIVE paths like /search (do NOT prefix /v1). EVERY call must send header Notion-Version: 2026-03-11 (Content-Type: application/json on POST/PATCH). Only pages/databases shared with the connection (page ... menu > Connections) are visible; unshared = 404.

READS
- POST /search {query, filter:{property:"object",value:"page"|"data_source"}, sort:{timestamp:"last_edited_time",direction:"descending"}} -> pages/data sources by title (POST = Confirm card).
- GET /databases/{database_id} -> data_sources[] {id,name}. Rows live in a data source; query by its id.
- GET /data_sources/{data_source_id} -> schema (property names/types). Read before filtering.
- POST /data_sources/{data_source_id}/query {filter, sorts, page_size, start_cursor} -> rows (page objects). Filter e.g. {"property":"Status","status":{"equals":"Done"}}, date {"past_week":{}}, {"and":[...]} (POST = Confirm card).
- GET /pages/{page_id} -> properties only. GET /pages/{page_id}/markdown -> page content as Markdown. GET /blocks/{page_id}/children -> raw blocks.

WRITES (Confirm card)
- POST /pages {parent:{data_source_id}|{page_id}, properties:{...}, markdown:"# ..."} -> new row/page (page parent: only "title" allowed).
- PATCH /pages/{page_id} {properties:{...}} -> update status/date/owner; in_trash:true trashes. Append content: PATCH /blocks/{page_id}/children {children:[...]}.

PAGINATION: has_more + next_cursor -> resend with start_cursor; page_size max 100; query caps at 10k rows.
LIMITS: ~3 req/s; 429 + Retry-After (sec); 500KB per request; rich_text 2000 chars.
IDs are UUIDs (dashes optional); the 32-hex tail of a Notion URL is the id. Values nest by type: title[0].plain_text, status.name, date.start, people[].name.

Unverified against official docs: Whether Free-plan workspaces can create internal connections: docs only say a Workspace Owner is required and do not mention plan tiers (historically the API has been available on all plans). · Exact workspace-wide rate limit numbers per plan (docs say it exists and scales with plan but give no figures). · Default capabilities assigned to a newly created internal connection (docs describe the capability options but not defaults; assume you must enable Read/Update/Insert content and user info explicitly). · The 'secret_' legacy prefix note comes from Notion's changelog/community sources rather than the current auth reference page (which shows ntn_ examples). · Whether the 32-hex tail of a Notion URL is always the page id vs a view id for database views was inferred from general Notion behavior, not re-verified in these docs.

Sources: [developers.notion.com](https://developers.notion.com/reference/intro)) · [developers.notion.com](https://developers.notion.com/docs/upgrade-guide-2025-09-03)) · [developers.notion.com](https://developers.notion.com/reference/post-search))


Airtable

Docs / Databases (CRM, pipeline, ATS, ops trackers built in Airtable). Lets Miles read, filter, and update the records in any Airtable base the token can reach — pipeline trackers, candidate lists, event RSVPs, content calendars — straight from Slack.

Try asking Miles:

  • @Miles pull every deal in our Airtable Pipeline base with Stage = 'Proposal' that hasn't been touched in 14 days and give me the owner + next step
  • @Miles who applied to the Founding AE role in the Airtable ATS this week? Summarize by source and flag anyone from a Series B+ company
  • @Miles add Sarah Chen (sarah@acme.com, Acme, Warm intro from Raj) as a new lead in the Contacts table with Status = 'New'
  • @Miles mark the 6 event RSVPs I just listed as 'Checked in' in the AIS6 Attendees table

Connect it today

  • Get a key: Available on ALL Airtable plans (Free included; Free = 1,000 API calls per workspace per month with a 30-day grace period, Team = 100,000/month then throttled to 2 req/s, Business/Enterprise Scale = no monthly cap). Create a personal access token at airtable.com/create/tokens (Developer hub -> Personal access tokens -> Create token): name it, add scopes (at minimum data.records:read + schema.bases:read; add data.records:write for creates/updates), then add the specific bases/workspaces (or 'all resources') the token may access. Token is shown once only and does not expire. The token inherits the creating user's permissions (editor access on a base is needed for writes). Enterprise admins may block PAT access to org-owned bases via an org setting.
  • Base URL: https://api.airtable.com/v0
  • Auth type: Bearer token — pat... (personal access token; opaque variable-length string beginning with its token ID, e.g. patXXXXXXXXXXXXXX.xxxx...)
  • Secret name (suggested): AIRTABLE_API_KEY
  • Auth note: Standard 'Authorization: Bearer <token>'. Legacy API keys (key...) and the ?api_key= query param were removed Feb 1 2024 — only PATs or OAuth tokens work. Miles sets Content-Type: application/json on JSON bodies automatically.

Good to know

  • Token must have BOTH the right scope (data.records:read / data.records:write / schema.bases:read) AND the specific base added as a resource — otherwise you get 403 ("model not found") or 404 even though the base exists.
  • You need a base ID (app...) and table ID/name to read anything. GET /meta/bases + GET /meta/bases/{baseId}/tables discover them; both need schema.bases:read. Base/table/view IDs are also visible in the Airtable URL (airtable.com/appX/tblY/viwZ).
  • filterByFormula must be URL-encoded and uses field NAMES (not IDs) wrapped in {curly braces}; a typo in a field name gives an INVALID_FILTER_BY_FORMULA 422. Fetch the schema first.
  • Array-style query params: single bracketed keys like sort[0][field]=X&sort[0][direction]=desc go in the query map. Repeated keys (fields[]=A&fields[]=B, records[]=rec1&records[]=rec2 for delete) need the query string in the path, e.g. path '/appXXX/Table?fields[]=Name&fields[]=Email' — Miles sends one value per query key otherwise.
  • Airtable offers POST /{baseId}/{tableIdOrName}/listRecords for long queries — it is a READ but because it is a POST it will stage a Confirm card in Miles. Prefer GET; keep URLs under 16k chars.
  • Rate limits: 5 requests/second per base and 50 requests/second across all traffic from one user's personal access tokens; exceeding returns HTTP 429 and you must wait 30 seconds before requests succeed again. Monthly call caps per workspace by plan: Free 1,000 (30-day grace, then blocked), Team 100,000 (then throttled to 2 req/s), Business/Enterprise Scale uncapped. Schema/meta calls count toward the cap. GET URLs must be shorter than 16,000 characters.
Usage notes — paste into the Connected API form
Airtable Web API. Bearer auth is injected; base URL is https://api.airtable.com/v0, so all paths are RELATIVE. Send Content-Type: application/json on writes.
IDs: base=app..., table=tbl... (table NAME also works), view=viw..., record=rec...
READS (GET, run immediately):
- GET /meta/bases -> bases[{id,name,permissionLevel}].
- GET /meta/bases/{baseId}/tables -> tables[{id,name,fields[{id,name,type,options}],views[{id,name}]}]. Call FIRST to get exact table/field names.
- GET /{baseId}/{tableIdOrName} -> {records:[{id,createdTime,fields}],offset?}. Params: filterByFormula (field names in {braces}, e.g. AND({Status}='Open',IS_AFTER({Created},DATEADD(TODAY(),-7,'days')))), view (name/ID; applies view filters+order), fields[]=Name (repeat), sort[0][field]=X&sort[0][direction]=desc, maxRecords, pageSize (max 100), offset. Empty fields omitted; no view/sort = arbitrary order.
- GET /{baseId}/{tableIdOrName}/{recordId} -> one record.
PAGINATION: response has "offset" while more pages exist; repeat with ?offset=<value>.
WRITES (stage a Confirm card):
- POST /{baseId}/{tableIdOrName} {"records":[{"fields":{"Name":"..."}}],"typecast":true} creates up to 10 records.
- PATCH /{baseId}/{tableIdOrName} {"records":[{"id":"rec...","fields":{...}}]} updates up to 10 (never PUT: it wipes unlisted fields). Upsert: add "performUpsert":{"fieldsToMergeOn":["Email"]}, omit id.
- DELETE /{baseId}/{tableIdOrName}?records[]=rec1 (max 10). Confirm first.
Cell formats: singleSelect=string, multipleSelects=[strings], linked records=[recordIds], date=ISO, checkbox=true. Formula/lookup/rollup are read-only.
LIMITS: 5 req/s per base; 429 => wait 30s. Free plan 1,000 calls/workspace/month. 403 = no base access/scope; 422 = bad field/value. Avoid POST /listRecords for reads (it stages a Confirm card).

Unverified against official docs: Exact 'pat...' token prefix: the official PAT guide only says tokens are 'prefixed with their ID' and should be treated as opaque; the 'pat' prefix (token ID = first 14 chars) comes from Airtable community/support snippets, not the reference page I fetched. · fields[]=Name repeated-key encoding for the list-records fields param comes from community examples; the official reference just lists 'fields' as an array and points to a URL-encoder tool (sort[0][field]/sort[0][direction] encoding IS confirmed in the official support article). · The 10-record batch cap is confirmed on Airtable's 'Managing API call limits' support page for create/update; the delete endpoint reference page does not state its own max, so 'max 10' for DELETE is inferred from the same support guidance. · Whether Miles' call_api query object can emit bracketed/repeated keys (fields[], sort[0][field], records[]) is a Miles-side capability I could not verify.

Sources: [airtable.com](https://airtable.com/developers/web/api/introduction)) · [airtable.com](https://airtable.com/developers/web/api/rate-limits)) · [airtable.com](https://airtable.com/developers/web/api/get-record))


Calendly

Scheduling. Lets Miles read your (or your whole team's) Calendly meetings, who booked them and what they answered, list booking links and open slots, and generate single-use links or cancel meetings on request.

Try asking Miles:

  • @Miles what Calendly meetings do I have booked this week, and who booked each one?
  • @Miles pull everyone who booked a demo through Calendly in the last 7 days with their email, company and what they wrote in the 'What do you want to cover?' question, and add them to the CRM board
  • @Miles which Calendly invitees no-showed or canceled last month, and how many rescheduled?
  • @Miles create a single-use link for my 30-min intro call and DM it to me so I can send it to the candidate

Connect it today

  • Get a key: Any Calendly plan including Free can create a personal access token (Free plan is rate-limited to 50 req/min; the Scheduling API POST /invitees, meeting recaps and webhooks need paid plans; activity log and data-deletion endpoints are Enterprise-only). Create the token in Calendly: Integrations page -> "API & Webhooks" tile -> "Get a token now" (or "Generate new token") -> name it -> pick scopes -> Create Token -> Copy token (shown once, unretrievable later). Token scope follows the user's role: a User-role token only sees that user's data; an Admin/Owner token can query organization-wide with organization=<orgURI>. New tokens are scoped: select at least users:read, scheduled_events:read, event_types:read, organizations:read, availability:read (+ scheduling_links:write and scheduled_events:write for the write endpoints).
  • Base URL: https://api.calendly.com
  • Auth type: Bearer token — Opaque personal access token, shown once; used raw after "Bearer "
  • Secret name (suggested): CALENDLY_API_KEY
  • Auth note: OpenAPI security scheme is http/bearer: 'Authorization: Bearer <TOKEN>'. Docs' cURL samples also send Content-Type: application/json, which only matters on POST bodies. Personal access tokens created after Calendly introduced scoped permissions grant NOTHING until scopes are selected at creation - a 403 usually means a missing scope, not a bad token.

Good to know

  • Calendly identifies everything by full URI (https://api.calendly.com/users/XXXX), not IDs. GET /users/me must be called first to get resource.uri (user) and resource.current_organization (org); /scheduled_events, /event_types, /organization_memberships etc. all need one of these as a query param.
  • The API is 'deterministic irrespective of requester': it never infers scope from the token. If Miles omits user= / organization= it gets an error or nothing, not the caller's own data.
  • organization=<orgURI> on /scheduled_events, /event_types, /organization_memberships requires the token owner to be an org Admin/Owner; a User-role token gets 403 and must use user=<own userURI>.
  • New personal access tokens are scoped at creation and grant no access until scopes are chosen; missing scope => 403. Legacy (pre-scope) tokens keep full access. Needed: users:read, scheduled_events:read, event_types:read, organizations:read, availability:read, plus scheduling_links:write / scheduled_events:write for the write calls.
  • Path UUIDs are the last segment of a resource uri (e.g. /scheduled_events/ABC123/invitees). Invitee URIs embed both event and invitee UUIDs.
  • Rate limits: User-based: 500 requests per user per minute on paid plans, 50 per user per minute on Free. Every response carries X-RateLimit-Limit / X-RateLimit-Remaining / X-RateLimit-Reset (seconds, typically 60); exceeding returns HTTP 429. Endpoint-specific caps on Create Event Invitee (Scheduling API): trial 5/day; paid non-Enterprise 10/min, 50/hr, 100/day; Enterprise 500/min. Max 8 OAuth token requests per user per minute (not relevant to PATs).
Usage notes — paste into the Connected API form
Auth is handled by Miles (Authorization: Bearer). Base URL is https://api.calendly.com, so all paths are RELATIVE. JSON; no extra headers.

BOOTSTRAP: Calendly uses full URIs, not IDs. Call GET /users/me first; cache resource.uri (user URI) and resource.current_organization (org URI). Lists need one as a query param. UUID = last path segment of a uri.

READS (run immediately):
- GET /scheduled_events?user=<userURI>&min_start_time=<ISO UTC>&max_start_time=<ISO UTC>&status=active|canceled&invitee_email=<email>&sort=start_time:asc&count=100 -> name, start/end_time (UTC), status, location, event_memberships (hosts), invitees_counter, uri. organization=<orgURI> instead of user= sees the whole team (admin/owner only).
- GET /scheduled_events/{uuid}/invitees?status=active&email=<email> -> name, email, questions_and_answers, timezone, rescheduled, cancel_url, reschedule_url, no_show.
- GET /event_types?user=<userURI>&active=true (or organization=; one required) -> name, slug, scheduling_url, duration, uri.
- GET /organization_memberships?organization=<orgURI>&email=<email> -> user uri + role.
- GET /event_type_available_times?event_type=<eventTypeURI>&start_time=<ISO>&end_time=<ISO> (future, <=31 days) -> open slots.

PAGINATION: keyset. collection[] + pagination.next_page_token; repeat with page_token=<token>; count max 100 (default 20); stop when next_page is null.

WRITES (stage a Confirm card in Slack):
- POST /scheduling_links body {"max_event_count":1,"owner":"<eventTypeURI>","owner_type":"EventType"} -> resource.booking_url.
- POST /scheduled_events/{uuid}/cancellation body {"reason":"..."} cancels for all invitees. No reschedule endpoint.

LIMITS: 500 req/user/min paid, 50 Free; back off on 429. Non-admin tokens see own data only (use user=). 403 = missing scope.

Unverified against official docs: Token string format/prefix: Calendly docs never state a prefix; the 'JWT-like, starts with eyJ' description is from experience, not documentation. · Exact response fields of /event_type_available_times (status, start_time, invitees_remaining, scheduling_url) were read from the OpenAPI schema name EventTypeAvailableTime but not expanded field-by-field. · Whether Content-Type: application/json is strictly required on POST bodies (docs' cURL samples include it; the spec does not mark it required).

Sources: [developer.calendly.com](https://developer.calendly.com/api-docs)) · [developer.calendly.com](https://developer.calendly.com/api-docs/ZG9jOjE1MDE3NzI-api-conventions)) · [developer.calendly.com](https://developer.calendly.com/api-docs/005832c83aeae-get-current-user))


Typeform

Forms. Lets Miles list your Typeforms and pull, filter, count and summarize form responses (waitlists, candidate applications, NPS/customer surveys, event intake) straight from Slack, plus create forms and webhooks on request.

Try asking Miles:

  • @Miles how many people submitted our beta waitlist Typeform in the last 7 days? Give me their emails and companies.
  • @Miles pull the applications from the 'AE - Sales Hire' Typeform since Monday and summarize what each candidate wrote for 'why do you want to join'.
  • @Miles for this month's NPS survey, what's the average score and who scored 6 or below? Include their email so I can follow up.
  • @Miles list every Typeform in our Recruiting workspace and how many responses each one has.

Connect it today

  • Get a key: Any Typeform account can use the APIs (no developer account or paid plan required for personal access tokens; official docs: "All you need is a Typeform account to use our APIs"). Create the token at Account (upper-left dropdown) -> Personal tokens -> Generate a new token (direct link admin.typeform.com/user/tokens); pick a name and the scopes it may use (at minimum forms:read + responses:read; add workspaces:read, accounts:read, forms:write, webhooks:read/write as needed). Scopes are fixed at creation and the token is shown once. Webhooks (and hidden fields, payments) require a paid plan per the docs' "PRO or PRO+" wording.
  • Base URL: https://api.typeform.com
  • Auth type: Bearer token — tfp_… (personal access token)
  • Secret name (suggested): TYPEFORM_API_KEY
  • Auth note: Standard 'Authorization: Bearer <tfp_ token>'. No API version header. Token permissions are limited to the scopes chosen when it was generated (403/401 if a scope is missing). Anyone with the token can read/update/delete forms and data, so scope it minimally. Personal tokens are valid for api.typeform.com and the older EU host api.eu.typeform.com, but the new EU host api.typeform.eu needs its own region-specific token.

Good to know

  • Responses are only retrievable per form: there is no cross-account responses endpoint, so Miles must GET /forms first to resolve the form_id (short id from the form URL) and then call /forms/{form_id}/responses.
  • Answers carry field ids/refs, not question text - call GET /forms/{form_id} once to map fields[].id/ref -> title when summarizing.
  • Very recent submissions (roughly the last 30 minutes) may not yet be returned by the Responses API; use webhooks for real-time.
  • Only completed responses are returned by default; pass response_type=completed,partial,started to include partials/abandons.
  • The advanced filter endpoint POST /forms/{form_id}/responses/search is a read done via POST, so in Miles it will stage a Confirm card - prefer GET /responses with query=/since=/until= for normal asks.
  • Rate limits: Official docs: 2 requests per second per Typeform account for the Create and Responses APIs (webhooks/embed unlimited). Miles should pace bulk per-form loops (e.g. counting responses across many forms).
Usage notes — paste into the Connected API form
Typeform API. Auth (Bearer) is injected; base URL is https://api.typeform.com, so use RELATIVE paths. No extra headers. Rate limit 2 req/sec. Last ~30 min of submissions may lag.

READS (GET):
- /me -> alias, email (connection check).
- /forms?search=&page=1&page_size=200&workspace_id= -> items[]{id,title,_links.display}. form_id = short id in the form URL.
- /forms/{form_id} -> fields[]{id,ref,title,type}, hidden[]. Maps answer field ids to question text.
- /forms/{form_id}/responses?since=&until=&page_size=25&before=&after=&response_type=completed&query= -> total_items, items[]{response_id,token,submitted_at,hidden{},answers[]{field{id,ref},type,text|email|number|boolean|date|phone_number|choice.label|choices.labels}}. since/until: ISO 8601 UTC. page_size default 25, max 1000. Completed only by default (response_type=completed,partial,started for partials). For a count use page_size=1 and read total_items.
- /workspaces?page=&page_size= -> items[]{id,name,forms.count}.

PAGINATION: /forms, /workspaces: page (1-based) + page_size (max 200). /responses: pass the last item's `token` as before= (older) or after= (newer).

WRITES (stage a Confirm card):
- PUT /forms/{form_id}/webhooks/{tag} body {"url":"https://...","enabled":true,"secret":"...","verify_ssl":true} -> create/update webhook (paid plan, webhooks:write).
- POST /forms body {"title":"...","fields":[{"title":"...","type":"short_text","ref":"q1"}]} -> new form; returns id + _links.display share URL (forms:write).
- POST /forms/{form_id}/responses/search is a read but a POST, so it also stages a card - prefer GET /responses with query=.

NOTES: Token scopes are fixed at creation (needs forms:read + responses:read). EU accounts: base URL must be api.eu.typeform.com or api.typeform.eu, else responses are empty.

Unverified against official docs: Insights endpoint GET /insights/{form_id}/summary (views, completion rate, drop-offs) - referenced by Typeform community threads with a Business-plan requirement, but the official reference page now 404s / redirects, so it may be deprecated or undocumented. · Exact current plan names gating webhooks: docs still say 'PRO or PRO+' (legacy naming); mapping to today's Basic/Plus/Business tiers not confirmed. · Whether personal access tokens expire (docs do not state an expiry). · Response payload for POST /forms - the docs' 'type' default was reported as 'quiz' by the fetch, which looks wrong; not included in notes.

Sources: [www.typeform.com](https://www.typeform.com/developers/get-started/)) · [www.typeform.com](https://www.typeform.com/developers/get-started/scopes/)) · [www.typeform.com](https://www.typeform.com/developers/get-started/responses-data-center/))


beehiiv

Newsletter. Lets Miles pull newsletter performance (open/click/unsubscribe stats per post and lifetime), look up or list subscribers and segments, and add/update subscribers in your beehiiv publication straight from Slack.

Try asking Miles:

  • @Miles how did our last three newsletters perform - open rate, click rate and unsubscribes for each?
  • @Miles is jane@acme.com subscribed to our newsletter, and when did she sign up / where did she come from?
  • @Miles how many active subscribers do we have on beehiiv right now and what's our lifetime average open rate?
  • @Miles add everyone who registered for tomorrow's webinar to our beehiiv newsletter with utm_source=webinar, no welcome email.

Connect it today

  • Get a key: API access is included on every beehiiv plan including the free Launch plan (Launch = "API Access (excluding Send API)"; Scale/Max/Enterprise = "API access"). The Send API (POST /publications/{id}/posts to create/send posts) is only available on Max and Enterprise. Keys are created in the beehiiv app: Settings -> API (under Workspace Settings) -> "Create New API Key"; the key is shown once only, and beehiiv may prompt for Stripe identity verification. A key can optionally be restricted to specific publications.
  • Base URL: https://api.beehiiv.com/v2
  • Auth type: Bearer token — No documented prefix; opaque API key string used as the Bearer token (Authorization: Bearer <key>)
  • Secret name (suggested): BEEHIIV_API_KEY
  • Auth note: Plain Bearer token. No account/workspace header needed. Publication-scoped keys return 404 for any publication outside their scope and GET /publications only lists the publications they can see. beehiiv also offers OAuth2 for third-party integrations, but for Miles use a personal API key.

Good to know

  • Almost every path is scoped under /publications/{publicationId} - Miles must call GET /publications first to find the pub_... ID (or the user should put it in the notes).
  • All read endpoints are plain GETs (no POST-style searches), so reads never trigger a Confirm card; the only PUT-style utility is PUT .../segments/{segmentId}/recalculate, which will stage a card.
  • The Send API (POST /publications/{publicationId}/posts, plus test-send/preview) is gated to Max and Enterprise plans; everything else works on the free Launch plan. Post creation is asynchronous (returns 201, then GET may return 202 with Retry-After).
  • expand *_content options on posts (free_web_content, free_email_content, premium_*_content) return full rendered HTML - slow and can blow through Miles' 1MB response cap; only expand stats.
  • Post stats aggregation can time out; subsequent requests may return consolidated click metrics instead of per-URL raw clicks.
  • Rate limits: 180 requests per minute per organization. Exceeding returns HTTP 429. Every response includes RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset (unix seconds) headers; beehiiv recommends exponential backoff.
Usage notes — paste into the Connected API form
beehiiv newsletter API. Auth (Bearer key) is handled by Miles; base URL is https://api.beehiiv.com/v2 so all paths are RELATIVE. No extra headers. JSON bodies. Timestamps = Unix seconds.
Most paths need a publication ID (pub_...). If unknown, GET /publications first and pick by name.

READS (GET, run immediately):
- /publications?expand[]=stats -> pubs + active_subscriptions, average_open_rate, average_click_rate, total_sent.
- /publications/{pub}/posts?expand[]=stats&status=confirmed&order_by=publish_date&direction=desc&limit=10 -> posts (title, subject_line, publish_date, web_url) + stats.email (recipients, unique_opens, open_rate, unique_clicks, click_rate, unsubscribes). Never expand *_content (huge HTML).
- /publications/{pub}/posts/{post_id}?expand[]=stats -> one post. /publications/{pub}/posts/aggregate_stats -> lifetime totals.
- /publications/{pub}/subscriptions?limit=100&status=active -> subscribers (email, status, subscription_tier, created, utm_*). Exact lookup: &email=x@y.com. Next page: &cursor=<next_cursor> while has_more.
- /publications/{pub}/subscriptions/by_email/{URL-encoded email} -> one subscriber (404 = not subscribed).
- /publications/{pub}/segments?expand[]=stats -> segments (id, name, total_results); /publications/{pub}/segments/{seg}/members -> members.
Pagination: limit 1-100 (default 10). Subscriptions use cursor; other lists use page=N.

WRITES (stage a Confirm card in Slack):
- POST /publications/{pub}/subscriptions {"email":"...","send_welcome_email":false,"utm_source":"miles"} -> add subscriber.
- PUT /publications/{pub}/subscriptions/by_email/{email} {"custom_fields":[{name,value}]} or {"unsubscribe":true}.
- POST /publications/{pub}/posts (Send API) needs Max/Enterprise; avoid unless asked.
Rate limit 180 req/min per org (429).

Unverified against official docs: API key format/prefix - beehiiv docs do not document a prefix; assumed to be an opaque string. · Whether array query params must be sent as expand[]=stats vs expand=stats - docs mix both spellings across pages (subscriptions list uses expand[], publications/posts list say expand as list); both are documented forms but I could not test which the server accepts on every endpoint. · Plan gating for API access is taken from beehiiv.com/pricing (Launch: 'API Access (excluding Send API)'; Send API on Max/Enterprise) and the Create Post doc page; not confirmed on a support-article page. · Whether cursor pagination is accepted on lists other than /subscriptions (pagination guide describes it generically, but the posts/segments/publications reference pages still document only page=).

Sources: [developers.beehiiv.com](https://developers.beehiiv.com/llms.txt)) · [developers.beehiiv.com](https://developers.beehiiv.com/welcome/getting-started.md)) · [developers.beehiiv.com](https://developers.beehiiv.com/welcome/rate-limiting.md))


Ashby

Recruiting / ATS. Lets Miles read jobs, applications, candidates, and interview schedules from your ATS, and add notes or move candidates between stages — every call asks for a Confirm because Ashby's API is all-POST.

Try asking Miles:

  • @Miles who's in the Onsite stage for the Account Executive role right now, and when are their interviews?
  • @Miles how many new candidates applied to open Engineering roles in the last 7 days, by source?
  • @Miles pull up jane@example.com in Ashby — which jobs, what stage, has the panel submitted feedback?
  • @Miles add a note to Marcus Lee's Ashby profile: 'Spoke today, comp expectations –200k, available in 4 weeks' and notify the hiring team

Connect it today

  • Get a key: An Ashby Admin creates the key at Admin → Integrations → API Keys. New keys start with NO permissions — tick per-module Read (and Write where needed): Jobs, Candidates, Interviews, Hiring Process, Organization. Optional toggles (confidential jobs, private fields, act-on-behalf) are off by default. Key is shown once. No plan tier requirement documented.
  • Base URL: https://api.ashbyhq.com
  • Auth type: HTTP Basic — Secret value must be the key followed by a colon — <ASHBY_API_KEY>: — (key as Basic username, blank password)
  • Secret name (suggested): ASHBY_API_KEY
  • Extra required header(s): Accept: application/json; version=1
  • Auth note: Docs: put the API key as the Basic username and leave the password blank. Missing key → 401; wrong key or missing endpoint permission → 403 missing_endpoint_permission.

Good to know

  • EVERY endpoint is POST (RPC style /resource.action) — there are no GETs — so every Ashby read stages a Confirm card in Slack. Ask Miles for everything you need in one go so it can batch.
  • Errors come back as HTTP 200 with {success:false, errorInfo}; only auth failures are 401/403.
  • Params go in the JSON body, never the query string. Date filters are unix MILLISECONDS.
  • New keys have zero permissions until you tick modules; confidential jobs and private fields need extra toggles.
  • Stage names are per interview plan: job.defaultInterviewPlanId → /interviewStage.list.
  • Rate limits: 1000 requests/min per key (help center).
Usage notes — paste into the Connected API form
Ashby is RPC-style: EVERY call is POST to a relative path like /application.list, even reads, so each Ashby call stages a Confirm card in Slack — batch per question. Auth is Basic (configured). Send header Accept: application/json; version=1. Params go in the JSON body (never query).
Envelope: {success, results, moreDataAvailable, nextCursor, syncToken}. Errors return HTTP 200 with success:false + errorInfo{code,message} — check success. Confidential jobs/private fields are hidden unless the key has those permissions.
Reads: /job.list {status:["Open"], limit} → id,title,status,departmentId,defaultInterviewPlanId,hiringTeam. /application.list {jobId, status:"Active", limit, cursor, createdAfter(unix ms)} → candidate{id,name,primaryEmailAddress}, job, currentInterviewStage{title,type}, source. /application.info {applicationId}. /candidate.search {email|name}. /candidate.list {createdAfter(unix ms), limit, cursor}. /candidate.info {id}. /interviewSchedule.list {applicationId|interviewStageId, createdAfter(unix ms), limit} → status + interviewEvents[startTime,endTime,interviewers,meetingLink,hasSubmittedFeedback]. /interviewStage.list {interviewPlanId} → ordered stages. /user.search {email}. /department.list, /source.list.
Pagination: limit max 100; resend nextCursor as cursor until moreDataAvailable=false. Cursors expire in 14 days. Date filters are unix ms.
Writes: /candidate.createNote {candidateId, note:"text", sendNotifications?}; /candidate.create {name(req), email, phoneNumber, linkedInUrl, sourceId}; /application.changeStage {applicationId, interviewStageId, archiveReasonId if archiving}; /candidate.addTag {candidateId, tagId}.
Rate limit: 1000 req/min per key.

Unverified against official docs: Rate limit is from the help center, not the developer reference. · No plan-tier gate documented; assumed available on all plans.

Sources: [developers.ashbyhq.com](https://developers.ashbyhq.com/reference/introduction)) · [developers.ashbyhq.com](https://developers.ashbyhq.com/docs/pagination-and-incremental-sync)) · [developers.ashbyhq.com](https://developers.ashbyhq.com/reference/interviewschedulelist))


Attio

CRM. Lets Miles read and filter your Attio people, companies, deals, lists, notes and tasks from Slack, and log notes or create follow-up tasks on CRM records after confirmation.

Try asking Miles:

  • @Miles which deals in Attio are still In Progress and who owns them? Link each one.
  • @Miles find Ada Lovelace in Attio and summarize the last 5 notes on her record.
  • @Miles what open Attio tasks are assigned to me with a deadline this week?
  • @Miles log a note on the Acme Corp company record in Attio with the recap from today's call, then create a task for me to follow up Friday.

Connect it today

  • Get a key: Available on all Attio plans, but only workspace admins can create tokens. In Attio: workspace name dropdown -> Workspace settings -> Developers tab -> "+ New access token" -> name it and pick scopes (at minimum record_permission:read, object_configuration:read; add note:read / task:read / list_entry:read / list_configuration:read / user_management:read for notes, tasks and lists; use *:read-write for the write endpoints). Tokens do not expire and can be viewed again later via the eye icon.
  • Base URL: https://api.attio.com/v2
  • Auth type: Bearer token — Opaque access token, no documented prefix (Basic auth with token as username and blank password is also accepted, but Bearer is the documented default)
  • Secret name (suggested): ATTIO_API_KEY
  • Auth note: Standard Authorization: Bearer <access_token>. Scopes are set per token in the Developers tab; a 403/missing-scope error means the token needs more scopes, not a different auth type. Bodies are JSON (Content-Type: application/json).

Good to know

  • Listing/filtering records (POST /objects/{object}/records/query), searching (POST /objects/records/search) and listing list entries (POST /lists/{list}/entries/query) are all reads that use POST, so Miles will stage a Confirm card in Slack for them; the notes tell Miles they are read-only and safe to confirm.
  • Docs moved from developers.attio.com to docs.attio.com (the old /reference URLs 308-redirect); the API host is unchanged at api.attio.com/v2.
  • Deals object exists in every workspace but is disabled by default; an admin must enable it in Objects settings before /objects/deals/... works. Deal stage values are workspace-customisable (defaults: Lead, In Progress, Won, Lost).
  • Token scopes are chosen when the admin creates the token; every endpoint lists required scopes (e.g. tasks need user_management:read in addition to task:read). Missing scopes surface as permission errors, not auth failures.
  • Record values are arrays with history (active_from/active_until); the current value is the entry with active_until null. Names are objects (first_name/last_name/full_name), emails are objects (email_address), stages are {status:{title}}.
  • Rate limits: 100 read requests/sec and 25 write requests/sec per token; 429 with a Retry-After header when exceeded (safe to retry). List records and list entries additionally use score-based limiting: a very complex query can be rejected outright, and many queries share a sliding 10-second budget across all apps using the same token.
Usage notes — paste into the Connected API form
Attio CRM. Auth (Bearer) is handled and base URL is set (api.attio.com/v2): use RELATIVE paths + JSON bodies. Objects: people, companies, deals (deals: admin must enable). Records include web_url; link it in replies.

READS
- GET /self - token check.
- POST /objects/{object}/records/query - body {filter, sorts, limit, offset}. Default limit 500; use 25-50. Filters: shorthand {"stage":"In Progress"}; ops $eq $contains $gt $gte $lt $lte $not_empty; logic $and $or $not; nested {"email_addresses":{"email_address":{"$eq":"a@b.com"}}}, {"domains":{"root_domain":"acme.com"}}. Sorts: [{"direction":"desc","attribute":"created_at"}]. Read-only despite POST (Confirm card is safe).
- POST /objects/records/search - {"query":"acme","objects":["people","companies"],"limit":10}. Fuzzy name/email/domain match. Also POST; beta.
- GET /objects/{object}/records/{record_id} - values: name[].full_name, email_addresses[].email_address, domains[].domain, stage[].status.title, value[].currency_value.
- GET /notes?parent_object=people&parent_record_id=<uuid>&limit=50 - title, content_plaintext (max 50).
- GET /tasks?is_completed=false&assignee=<email>&sort=created_at:desc&limit=50 - content_plaintext, deadline_at.
- GET /lists then POST /lists/{list}/entries/query {limit, offset}.

WRITES (Confirm card)
- POST /notes {"data":{"parent_object":"people","parent_record_id":"<uuid>","title":"...","format":"markdown","content":"..."}}
- POST /tasks {"data":{"content":"...","format":"plaintext","deadline_at":"ISO or null","is_completed":false,"linked_records":["person@co.com"],"assignees":[{"workspace_member_email_address":"me@co.com"}]}}

Pagination: limit/offset (done when results < limit). Limits: 100 reads/s, 25 writes/s; 429 + Retry-After. Values keep history; use active_until null.

Unverified against official docs: Exact value-object field names for deal stage (stage[].status.title) and deal value (value[].currency_value) were inferred from Attio's attribute-type conventions, not read verbatim from the deals object page - the people name (first_name/last_name/full_name) and email (email_address) shapes were verified. · Whether records/query enforces a hard maximum on limit (docs only state default 500). · Sorting by created_at via {"attribute":"created_at"} follows the documented sorts syntax but was not shown as an explicit example. · Access-token format/prefix: docs describe an opaque Bearer token with no documented prefix. · Whether the linked_records object form for tasks is exactly {target_object, target_record_id} - docs say 'object/ID reference' alongside email/domain strings.

Sources: [docs.attio.com](https://docs.attio.com/llms.txt)) · [docs.attio.com](https://docs.attio.com/rest-api/guides/authentication.md)) · [docs.attio.com](https://docs.attio.com/rest-api/guides/rate-limiting.md))


GitHub

Dev. Lets Miles read your GitHub issues, pull requests, commits and cross-repo search results from Slack, and (with a Confirm card) file issues or comment on them — so engineering status can flow into sprint boards, standups and investor/customer updates without anyone opening GitHub.

Try asking Miles:

  • @Miles what open issues are assigned to me across our GitHub repos and which were updated in the last 48 hours?
  • @Miles list the open PRs on acme/app that have been waiting more than 3 days for review, oldest first, and add them to this week's sprint board
  • @Miles what shipped in the miles repo since Monday? Summarize the commits into a 5-bullet changelog I can paste into the customer update
  • @Miles file a GitHub issue in acme/app titled 'Recap emails failing for free plan' with the notes from this thread, label it bug, and assign it to jane

Connect it today

  • Get a key: Any GitHub account (Free included) can create a personal access token. Create a fine-grained PAT at github.com: profile picture > Settings > Developer settings > Personal access tokens > Fine-grained tokens > Generate new token. Pick the resource owner (your user or an org), choose All repositories or Only select repositories, and grant repository permissions: Metadata (read, auto), Issues (read, or read+write for creating issues/comments), Pull requests (read), Contents (read, for commits/releases). Set an expiration (orgs may enforce a max lifetime). For org-owned repos, the org must allow fine-grained PATs (enabled by default since GA, Mar 2025) and may require an org owner to approve the token before it works. A classic PAT (ghp_) with repo scope also works but is not recommended.
  • Base URL: https://api.github.com
  • Auth type: Bearer token — Fine-grained PAT: github_pat_… (recommended); classic PAT: ghp_…
  • Secret name (suggested): GITHUB_API_KEY
  • Extra required header(s): X-GitHub-Api-Version: 2022-11-28
  • Auth note: GitHub accepts 'Authorization: Bearer <token>' (also 'Authorization: token <token>'; Bearer is what Miles sends and is fine). X-GitHub-Api-Version is technically optional (omitting it defaults to 2022-11-28) but GitHub recommends sending it explicitly; a newer version 2026-03-10 exists with breaking field removals — stick with 2022-11-28. Accept: application/vnd.github+json is recommended but not required. Username/password basic auth is NOT supported.

Good to know

  • All Issues endpoints (GET /issues, GET /repos/{owner}/{repo}/issues, GET /search/issues) return pull requests mixed in with issues; PRs are identifiable by a pull_request key (or use is:issue / is:pull-request in search).
  • GitHub returns 404 (not 403) for private repos the token cannot access — a 404 usually means the fine-grained PAT was not granted that repo/org or its permissions, not that the repo does not exist.
  • Fine-grained PATs must be scoped to a resource owner (user OR one org) and to specific repos/permissions; org-owned repos may require an org owner to approve the token, and orgs can disable fine-grained PATs or enforce max expiration. Token expiry will silently break the connection later.
  • Fine-grained PAT needs the right repository permissions: Issues (read/write), Pull requests (read), Contents (read for commits/releases), Metadata (read). Search only returns items from repos the token can access plus public repos.
  • GET /search/issues: since Sept 4 2025 all queries use advanced search (AND/OR/nesting). GitHub Apps with user tokens get 422 unless the query includes is:issue or is:pull-request — safe to always include one. Search is limited to 30 req/min and 1,000 results.
  • Rate limits: Primary: 5,000 requests/hour per personal access token (unauthenticated 60/hour; GitHub App installations 5,000 or 15,000 on Enterprise Cloud). Response headers x-ratelimit-limit / -remaining / -used / -reset (UTC epoch seconds) / -resource; GET /rate_limit shows status without counting against the primary limit. Search endpoints (GET /search/*): 30 requests/minute authenticated (semantic/hybrid search_type: 10/min); code search 10/min. Secondary limits: max 100 concurrent requests, max 900 points/minute for REST (GET=1 point, writes=5), no more than 80 content-creating requests/minute and 500/hour. On 403/429 with a retry-after header, wait that many seconds before retrying.
Usage notes — paste into the Connected API form
GitHub REST API. Auth (Bearer PAT) is injected; base URL is https://api.github.com so paths are RELATIVE and start with "/". Send header X-GitHub-Api-Version: 2022-11-28 on every call. Repos are /repos/{owner}/{repo} ({owner} = user or org login).

READS (GET, run immediately):
- /issues?filter=assigned&state=open&per_page=50 - issues assigned to the token owner across all repos. filter=assigned|created|mentioned|all; state=open|closed|all; labels=a,b; since=ISO8601; sort=created|updated.
- /repos/{owner}/{repo}/issues?state=open&per_page=50 - repo issues; params labels, assignee, creator, milestone, since, sort. Results include PRs: drop items with a "pull_request" key if the user wants issues only.
- /repos/{owner}/{repo}/pulls?state=open&per_page=50 - PRs; state=open|closed|all, base=main, sort=created|updated|long-running; draft:true = draft.
- /search/issues?q=QUERY&sort=updated&order=desc&per_page=50 - cross-repo search. Qualifiers: repo:owner/name, org:name, is:issue or is:pull-request, is:open, assignee:login, label:bug, updated:>=YYYY-MM-DD; AND/OR/() allowed. Max 1000 results, 30 req/min.
- /repos/{owner}/{repo}/commits?since=ISO8601&per_page=50 - recent commits (author=login, until, sha=branch).

WRITES (stage a Confirm card in Slack):
- POST /repos/{owner}/{repo}/issues body {"title","body","labels":[],"assignees":[]} - create issue.
- POST /repos/{owner}/{repo}/issues/{issue_number}/comments body {"body"} - comment on an issue/PR.

Pagination: page + per_page (default 30, max 100); increment page until a page comes back short. Keep per_page <=50: issue JSON is large and responses are capped at 1MB.
Rate limits: 5,000 req/hour per PAT; search 30/min; <=80 content-creating requests/min. A 404 on a repo usually means the token lacks access.

Unverified against official docs: Exact fine-grained permission for GET /issues (authenticated user's issues) — the issues doc excerpt said 'issues:read' but the endpoint was not listed in the permissions-reference table I fetched. · Whether the fine-grained permission for POST /repos/{owner}/{repo}/issues/{issue_number}/comments on a pull request is Issues:write or Pull requests:write (docs excerpt did not state it). · Approximate per-item JSON size (3–8 KB) used for the per_page<=50 recommendation is from experience, not from docs. · The full breaking-change list for API version 2026-03-10 was summarized by a fetch helper; the specific fields cited (assignee, merge_commit_sha, has_downloads) came from that summary.

Sources: [docs.github.com](https://docs.github.com/en/rest/authentication/authenticating-to-the-rest-api?apiVersion=2022-11-28)) · [docs.github.com](https://docs.github.com/en/rest/about-the-rest-api/breaking-changes?apiVersion=2026-03-10)) · [docs.github.com](https://docs.github.com/en/rest/issues/issues?apiVersion=2022-11-28))


Not yet (and why)

  • Salesforce, Greenhouse Harvest v3, Outreach — OAuth client-credentials with expiring tokens. Connected APIs inject a static secret today, so these need token-refresh support first. (Greenhouse's Basic-auth v1/v2 API is unavailable after Aug 31, 2026 — use Ashby or Lever, which use Basic with the key as username.)
  • Linear, and other GraphQL APIs — technically work (raw token in the Authorization header → Header type), but every query is a POST, so every read asks for a Confirm.
  • Apollo.io — Miles already enriches contacts with Apollo natively; no connection needed.

Ideas for the next batch

Mercury / Brex (cash balance, transactions — Bearer) · Intercom (Bearer) · Zendesk (Basic email/token:<key>) · PostHog (Bearer) · Close CRM (Basic <key>:) · Gong (Basic) · Lemlist / Smartlead (Basic / Query) · Eventbrite (Bearer) · Asana / ClickUp (Bearer / Header) · Kit / Loops / Mailchimp.

Last verified against vendor docs: August 2026. APIs change — if a call 4xxs, check the vendor's changelog first, then update the usage notes (Miles can do this itself with update_api_instructions).