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 APIs → Name, 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
| App | Category | Auth type | Plan needed for API | Reads instant? |
|---|---|---|---|---|
| Luma | Events | Header — x-luma-api-key | Requires an active Luma Plus subscription on the calendar you want to | Yes |
| Stripe | Payments / Billing | Bearer token | Any Stripe account (no paid plan) | Yes |
| Granola | Meeting notes | Bearer token | Business or Enterprise plan | Yes |
| Pipedrive | CRM | Header — x-api-token | Any paid Pipedrive plan (Lite/Growth/Premium/Ultimate) — every user ha | Yes |
| Fathom | Meeting notes | Header — X-Api-Key | Included on all Fathom plans (free and paid) per the official FAQ | Yes |
| HubSpot | CRM | Bearer token | Any HubSpot tier, including Free CRM | Yes |
| Notion | Docs / Wiki / Project management (pages + databases) | Bearer token | Create an internal connection in the Notion Developer portal (https:// | Mostly (some reads are POST → Confirm) |
| Airtable | Docs / Databases (CRM, pipeline, ATS, ops trackers built in Airtable) | Bearer token | Available on ALL Airtable plans (Free included | Yes |
| Calendly | Scheduling | Bearer token | Any Calendly plan including Free can create a personal access token (F | Yes |
| Typeform | Forms | Bearer token | Any Typeform account can use the APIs (no developer account or paid pl | Yes |
| beehiiv | Newsletter | Bearer token | API access is included on every beehiiv plan including the free Launch | Yes |
| Ashby | Recruiting / ATS | HTTP Basic | An Ashby Admin creates the key at Admin → Integrations → API Keys | No — all reads are POST → Confirm |
| Attio | CRM | Bearer token | Available on all Attio plans, but only workspace admins can create tok | Mostly (some reads are POST → Confirm) |
| GitHub | Dev | Bearer token | Any GitHub account (Free included) can create a personal access token | Yes |
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 (orcompany_domainfromGET /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,
archivedinstead ofin_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
Authorizationheader → 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).