Loops ships one REST API at https://app.loops.so/api under a v1 prefix, an OpenAPI 3.1.0 document covering 47 paths, 5 SDKs, a CLI and, unusually for an email platform this size, a first-party Model Context Protocol server at https://mcp.loops.so. That server is the headline here. The API itself is conventional bearer-token JSON, with two documented absences: no pagination contract, no idempotency contract.
The API still adds whole resource families every few weeks, and it is not a mature enterprise-contract API: no v2, no published deprecation window, no scoped credentials. Teams arriving from SendGrid or Mailgun should budget for that.
The Model Context Protocol is how an AI agent reaches a third-party product without a bespoke integration. Loops publishes a hosted server for it, which puts it in a small minority of email vendors.
Loops runs a first-party remote MCP server at https://mcp.loops.so over Streamable HTTP, authenticated with OAuth through Client ID Metadata Documents rather than a pasted key. Loops describes the coverage as managing contacts and mailing lists, sending transactional emails, working with events and reaching the rest of the API, effectively the whole REST surface.
A browser OAuth handshake means no long-lived key in an agent config file, and binds the connection to a person rather than a shared secret. The trade is that tool calls go to Loops infrastructure; there is no self-hosted build.
The server exposes 4 tools, not one per endpoint: Search finds the right API operation, Describe inspects its parameters, Execute runs it against a named team, Teams lists the teams the user can reach. Every REST call goes through Execute.
The design is unusual and correct here. One tool per endpoint would push 47 definitions, each with a full JSON Schema, into the agent’s context before the first user message is read, a large share of the window spent on definitions the agent never calls, with selection accuracy degrading as the list grows. Four tools cost almost nothing and stay constant.
The price is round trips: creating a contact means Search, then Describe, then Execute – 2 round trips before the first useful call, a third if Teams must resolve the team. Invisible interactively, costly in a high-frequency loop where you should call REST directly. The upside: when Loops added workflow endpoints in July 2026, no server release was needed for an agent to find them.
Commands as published by Loops. The Claude Code one installs at user scope, so the connector is available in every project.
# Claude Code claude mcp add loops https://mcp.loops.so --scope user --transport http # Codex CLI (v1.48.0 and later) codex mcp add loops --url https://mcp.loops.so
For Claude Desktop, Loops sits under Settings › Connectors › Loops; for ChatGPT, in the plugin marketplace. The first call opens a browser window for the OAuth grant.
One official server, plus third-party wrappers reaching Loops through automation platforms. Those are not supported by Loops and cover whatever the wrapping platform exposes, typically a small subset of the 47 paths.
First-party remote server, Streamable HTTP with OAuth. Four meta tools proxy the REST API.
Zapier’s Loops actions as MCP tools. Coverage is Zapier’s action set, not the API.
Loops toolkit in Composio’s managed catalogue; Composio holds credentials.
Loops app on Pipedream’s hosted MCP endpoints, sharing existing Pipedream credentials.
Third-party iPaaS wrapper; the tool list is viaSocket’s, not the OpenAPI surface.
Community connector in the Glama directory. Unaffiliated; verify its scope before granting a key.
Name collision. Several MCP servers published as Loops have nothing to do with loops.so: the smithery.ai entry under loops/loops-server, the useloops.io product, the smogili1/loop_mcp repository and the Loopy MCP Server are unrelated software, and directory search conflates them. The only first-party Loops email server is https://mcp.loops.so.
Why buyers should care. An official MCP server changes what a small team can do without engineering time: contact hygiene, list checks and a one-off transactional test become plain-language requests instead of tickets, and the OAuth grant, scope and uptime are the vendor’s responsibility. It remains rare in 2026: most email platforms are still reached through community or iPaaS wrappers rather than a first-party server.
The contract is small and conventional. The last two rows record what the documentation does not say.
| Base URL | https://app.loops.so/api |
| Path prefix | v1 – every documented endpoint sits under it |
| Format | JSON request bodies, JSON responses |
| OpenAPI spec | https://app.loops.so/openapi.json – OpenAPI 3.1.0, 47 paths |
| Spec version | 1.21.12 as read on September 14, 2026. A rolling build number, not a stable API version. |
| Pagination | Not documented |
| Idempotency | Not documented |
Those are real absences, not omissions on our side: no cursor, page or limit parameter on any list endpoint, and no idempotency key header on any write endpoint. List contacts on a growing account and you have no guarantee how many rows return; a timed-out POST has no documented safe retry. Version against the prefix, never the build number.
Loops pushes events out over HTTP with a Standard Webhooks-shaped signature. Events cover the contact lifecycle (contact.created, contact.unsubscribed, contact.deleted, plus mailing list subscribe and unsubscribe), the send lifecycle (campaign.email.sent, loop.email.sent, transactional.email.sent) and the delivery lifecycle (email.delivered, email.softBounced, email.hardBounced, email.opened, email.clicked, email.unsubscribed, email.resubscribed, email.spamReported), plus testing.testEvent.
Verification is HMAC-SHA256 over id.timestamp.rawBody with a secret prefixed whsec_, using headers Webhook-Signature, Webhook-Id and Webhook-Timestamp. Sign the raw bytes before any JSON parsing, and compare in constant time. The payload carries eventName, webhookSchemaVersion 1.0.0 and eventTime.
Delivery is capped at 10 events per second, your endpoint must respond within 15 seconds, and Loops retries with exponential backoff up to 8 attempts across roughly 28 days. That generous window has a consequence: a four-week-old event can arrive after a newer one, so consumers must de-duplicate on Webhook-Id and never assume ordering. The timeout means enqueue and return, never process inline.
One mechanism for the REST API, a different one for the MCP server. The split trips people up.
Every REST call carries Authorization: Bearer YOUR_API_KEY, generated in the dashboard. A missing, malformed or revoked key returns 401. No HTTP Basic fallback, no query-string key, no signed-request scheme.
A dedicated endpoint confirms a key works: GET /v1/api-key returns success and teamName. That second field names the team whose data you are about to write to, catching the staging-key-in-production mistake before anything sends.
This is the notable asymmetry. The MCP server authenticates with OAuth and a browser consent flow; the REST API does not offer OAuth at all. A product acting on your own users’ Loops accounts therefore has no authorisation-code flow, each customer generates and pastes a long-lived key, with the handling obligations that implies. Mailchimp and Klaviyo publish OAuth for this case. Loops does not.
No documented scopes. No permission scopes, no read-only keys, no per-endpoint restrictions, so assume any valid key reaches all 47 paths, contact deletion and suppression changes included. Never embed a Loops key where a browser or mobile client can read it, and note that separate keys for reporting and sending restrict neither, they only make revocation cheaper.
Published, per team, enforced by rejection rather than queueing.
| Limit | Value | Scope and notes |
|---|---|---|
| Standard API | 10 requests per second | Per team, across the standard endpoints |
| Content API | 60 requests per 60 seconds | Per team; a separate, tighter budget |
| Response headers | x-ratelimit-limit, x-ratelimit-remaining | Let a client track its own budget |
| Over-limit behaviour | HTTP 429 | Rejected outright. No documented queueing. |
| Reset signalling | Not documented | No x-ratelimit-reset, no retry-after |
| Free plan sending | 10 emails per second | Published on the pricing page |
| Webhook delivery | 10 events per second | Outbound, Loops to your endpoint |
The consequence of that fifth row: client-side backoff is mandatory. The headers show remaining budget, but when a 429 lands nothing says when the window reopens. Retry immediately and you burn the next window; retry at a fixed interval across several workers and they synchronise into a thundering herd. Use exponential backoff with full jitter, from 1 second to about 30, plus a token bucket below 10 requests per second.
Limits are per team, not per key: a second key buys no throughput, and a batch importer shares its 10 requests per second with the live signup handler.
Five official packages under the loops-so GitHub organisation, plus a CLI and agent skills.
| Language | Package | Install | Repo |
|---|---|---|---|
| JavaScript / TypeScript | loops on npm | npm install loops | loops-so/loops-js |
| PHP | loops-so/loops on Packagist | composer require loops-so/loops | loops-so/loops-php |
| Ruby | loops_sdk on RubyGems | gem install loops_sdk | loops-so/loops-rb |
| Go | github.com/loops-so/loops-go | go get github.com/loops-so/loops-go | loops-so/loops-go |
| Nuxt (module) | nuxt-loops on npm | npm install nuxt-loops | Not documented |
The JavaScript SDK requires Node 18.0.0 or later, the only runtime floor Loops publishes.
No published version numbers. Loops states no current version for any of the 5 SDKs: no version table, no minimum-supported-version statement, no deprecation notice. You can read the version from npm, Packagist, RubyGems or the Go proxy, but not whether it is the one the docs describe. Pin exact versions and read repository history before upgrading.
This is the gap that catches Django and FastAPI teams. Loops publishes no PyPI package and no Python client; the documented path is SMTP, with an official Django guide. One caveat, covered in full on this platform’s SMTP settings tab: that relay still expects an API-shaped payload carrying a transactionalId, so it is not a drop-in mail backend. For a native client, generate one from the OpenAPI document.
The official CLI carries 17 subcommands – auth, contacts, campaigns, campaign-groups, audience-segments, components, contact-properties, email-messages, event-patterns, events, lists, themes, transactional, transactional-groups, uploads, workflows, completion – mapping onto the REST resource families, which makes it a fast way to explore the API before writing code. Loops also publishes agent skills: packaged instructions giving a coding agent working knowledge of the platform without a tool connection.
The 47 documented paths group into roughly a dozen resource families. The OpenAPI document is authoritative for the full list.
| Resource | Methods | Description |
|---|---|---|
| API key test /v1/api-key | GET | Validates the token, returns success and teamName. |
| Dedicated sending IPs /v1/dedicated-sending-ips | GET | Lists dedicated IPs; commercial terms are not published. |
| Contacts /v1/contacts/create, /update, /find, /delete | GET, POST, PUT | Contact lifecycle, in verb-in-path style rather than REST-on-collections. |
| Suppression list /v1/contacts/suppression | GET, DELETE | Reads and clears suppressions; check before debugging a non-delivery. |
| Contact properties /v1/contacts/properties | GET, POST | Lists and creates custom properties. |
| Mailing lists list mailing lists | GET | Enumerates lists, whose IDs carry subscription state. |
| Events send an event, event patterns | GET, POST | Sends behavioural events; reads event patterns by name or ID. |
| Transactional send /v1/transactional | POST | Sends one email by transactionalId with a dataVariables object. |
| Transactional templates list, create, get, update, publish | GET, POST | Template management, with ensure-draft and publish steps. |
| Transactional groups list, create, get, update | GET, POST | Groups templates; carries transactional preferences. |
| Campaigns /v1/campaigns, /{campaignId} | GET, POST | Campaign CRUD, with audience targeting and scheduling. |
| Audience segments /v1/audience-segments, /{audienceSegmentId} | GET, POST | Lists and creates segments, reads one by ID. |
| Email messages and Guardian get, update, send preview, Guardian | GET, POST | Edits content, sends previews, runs Guardian checks. |
| Themes and components list, create, get, update | GET, POST | Design themes and reusable content components. |
| Workflows (open alpha) workflows, nodes, branches, connections | GET, POST, DELETE | Workflow and node CRUD, branches, connection rerouting, recursive deletion. Open alpha: the contract can change without notice. |
Contact paths are action-oriented, not collection-oriented, so code generators assuming REST conventions need the OpenAPI document. And the newest families are open alpha: isolate them behind your own interface.
Two calls worth memorising: proving the key works, and sending one transactional email.
Run this first in every environment: a 200 with a teamName you recognise means the key is live and aimed at the right team.
curl -s https://app.loops.so/api/v1/api-key \
-H "Authorization: Bearer YOUR_API_KEY"
# { "success": true, "teamName": "Your Team" }
# A bad, revoked or missing key returns HTTP 401.
Transactional sends are addressed by template: you pass a transactionalId for a template that already exists, the recipient address, and a dataVariables object with the values the template interpolates. There is no way to send ad hoc content here.
curl -s -X POST https://app.loops.so/api/v1/transactional \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"transactionalId": "YOUR_TRANSACTIONAL_ID",
"email": "customer@example.com",
"dataVariables": {
"firstName": "Ada",
"resetUrl": "https://example.com/reset/abc123"
}
}'
The official JavaScript SDK wraps the same call as sendTransactionalEmail with an identical argument object, on Node 18.0.0 or later.
Wrap both in a retry that backs off on 429, and treat the transactionalId as configuration: template IDs differ between staging and production, and a hardcoded one is the commonest cause of a reset email rendering the wrong template.
Searching an MCP directory for Loops returns unrelated products: a loops-server entry on smithery.ai, the useloops.io service, the smogili1/loop_mcp repository, a Loopy MCP Server. None are loops.so, and installing one then wondering why your contacts never appear is a predictable half hour lost. Verify the host.
Nothing describes a cursor, offset, page parameter or result cap for any list endpoint. On a new account that is invisible; on one with tens of thousands of contacts, a list call either returns an unbounded payload or silently truncates, with no way to tell which. Prefer targeted reads such as /v1/contacts/find, and drive bulk state from webhooks.
The budget headers help you avoid the ceiling, but nothing documents x-ratelimit-reset or retry-after on the rejection, and there is no queue: an over-limit request is rejected, not delayed. The client owns the retry policy entirely, so backoff with jitter is not optional, without it a multi-worker fleet retries in lockstep and stays over the limit.
The workflow family is open alpha, as were the transactional create and edit endpoints in June 2026. The shape can change with no deprecation window, and two of the eight changelog entries last quarter touched workflows, one of them changing a response guarantee. Pin the behaviour you depend on in a test.
Loops returns the literal string Some body key or value is longer than allowable when a body exceeds its limits. The message names neither the offending key nor the threshold, and the limit is not published. It bites on dataVariables carrying long rendered strings. Validate lengths client-side, log payload size and the largest value on failure, and bisect by removing fields.
The current quarter of the official changelog, newest first. The pattern is a platform adding whole resource families rather than refining existing ones.
rootNodeId.transactionalName added to transactional webhook events.No deprecations are listed, and Loops publishes no policy on how much notice a breaking change gets. With two families in open alpha, follow the official API changelog rather than waiting to be told.
Yes, at https://mcp.loops.so, over Streamable HTTP with OAuth through Client ID Metadata Documents. Be careful with directory listings: unrelated products are published under the name Loops.
Because the API has 47 paths, and one tool per endpoint would spend a large share of an agent’s context window on schemas it never uses, degrading tool selection. Search, Describe, Execute and Teams proxy the whole API at constant cost, at the price of two extra round trips before the first useful call.
No. It accepts only Authorization: Bearer YOUR_API_KEY and returns 401 otherwise. OAuth exists on the MCP server only, so a product acting on customers’ accounts needs each customer to hand over a long-lived key.
No. Loops publishes SDKs for JavaScript and TypeScript, PHP, Ruby, Go and a Nuxt module, five, no PyPI package. The documented Python path is SMTP, with an official Django guide. For a native client, generate one from https://app.loops.so/openapi.json.
10 requests per second per team on standard endpoints, 60 per 60 seconds on the Content API. Responses carry x-ratelimit-limit and x-ratelimit-remaining. Over-limit requests are rejected with 429, with no queue and no documented reset header, so client-side backoff with jitter is required.
Compute HMAC-SHA256 over id.timestamp.rawBody with the secret prefixed whsec_, then compare in constant time against Webhook-Signature, using Webhook-Id and Webhook-Timestamp for the other components. Sign the raw bytes before any JSON parsing. Respond within 15 seconds and de-duplicate on Webhook-Id: Loops retries up to 8 times over roughly 28 days, out of order.
This review follows our email infrastructure testing methodology. We disclose affiliate relationships in our editorial independence policy.