Loops website screenshot

Loops

Loops is a modern email platform purpose-built for software companies, combining product, marketing, and transactional email behind a single REST API and a single audience. Contacts, contact properties, mailing lists, events, segments, campaigns, email messages, themes, components, uploads, transactional sends, and dedicated sending IPs are all first-class API resources, with an OpenAPI 3.1 spec published at app.loops.so/openapi.yaml. Lifecycle workflows ("loops") use the Events API as their trigger, an HMAC-SHA256-signed webhook stream broadcasts every contact and email event for real-time integrations, and official SDKs ship for JavaScript, Nuxt, PHP, Ruby, and Go alongside an official Go-based CLI. Transactional email is included on every plan, team seats are unlimited, and a generous free tier covers 1,000 contacts and 4,000 sends per month — making Loops a Tier-1 developer-first email service for SaaS teams from waitlist to IPO.

1 APIs 26 Features
EmailMarketing EmailTransactional EmailEmail AutomationEmail CampaignsEmail WorkflowsContactsAudience ManagementEventsWebhooksSaaSDeveloper Tools

APIs

Loops API

The Loops API is a single REST API exposing every Loops resource — contacts, contact properties, mailing lists, events, campaigns, email messages, themes, components, transactio...

Collections

Features

Unified platform for product, marketing, and transactional email built for software companies
Single REST API (OpenAPI 3.1 published at app.loops.so/openapi.yaml) covering contacts, properties, mailing lists, events, campaigns, email messages, themes, components, transactional, uploads, suppressions, and dedicated sending IPs
Bearer-token API key authentication with a GET /api-key test endpoint
10 requests/second per team baseline rate limit with x-ratelimit-* response headers
Contacts API — create, update, find, delete; manage suppression status
Contact properties API — create custom properties and list the full property catalog
Mailing lists API — list mailing lists and manage list membership via contact updates
Events API — POST /events/send to trigger loops (workflows) and personalize content
Campaigns API — full CRUD plus list, with separate endpoints for the email message body
Email message API — fetch and update the content of an individual email message inside a campaign
Themes and components API — read reusable themes and shared components used across emails
Transactional email API — send transactional emails with template variables, file attachments, and headers; list transactional sends
Uploads API — create and complete file uploads for use in transactional attachments
Dedicated sending IPs API — list dedicated IPs assigned to your account
Webhook system with HMAC-SHA256 signing, 30 days of delivery history, contact + email + send + testing events, schema version 1.0.0
Lifecycle workflows ("loops") with triggers, timers, branching, and conditional logic
Saved segments built from contact properties and engagement data
A/B testing for campaigns
Email design editor optimized for major email clients
Analytics for conversion, opens, clicks, bounces, and unsubscribes
Official SDKs for JavaScript/TypeScript, Nuxt, PHP, Ruby, and Go; community SDKs for Laravel and Rails
Official `loops` CLI (Go) with Homebrew tap distribution
Official Claude Code Skills repository for the Loops API and CLI
25+ integrations across Stripe, Supabase, Clerk, Auth0, Auth.js, Better Auth, PostHog, Segment, RudderStack, Fivetran, HubSpot, Salesforce, Attio, Clay, Zapier, Make, Integrately, Bubble, Framer, Webflow, Emailify, and Email Love
Transactional email included at no extra charge on every plan
Unlimited team seats — never billed per seat or per send

Integrations

Stripe

Sync Stripe customers to your Loops audience and trigger automated emails on billing events.

Supabase

Send Supabase authentication emails (magic links, password reset, confirmations) through Loops.

Clerk

Send Clerk authentication emails through Loops.

Auth0

Route Auth0 authentication emails through Loops.

Auth.js

Send Auth.js authentication emails through Loops.

Better Auth

Route Better Auth transactional emails through Loops.

PostHog

Sync PostHog events and identified users into Loops contacts and workflows.

Segment

Sync Segment events and identified users into Loops.

RudderStack

Sync RudderStack events into Loops as event triggers.

Fivetran

Sync data into Loops via Fivetran connectors.

HubSpot

Sync HubSpot contacts to Loops.

Salesforce

Sync Salesforce contacts to Loops.

Attio

Sync Attio CRM contacts to Loops.

Clay

Push contacts from Clay into Loops.

Zapier

Connect Loops to 6,000+ apps via Zapier for contact sync, event triggers, and email sends.

Make

No-code automation flows that read from and write to Loops.

Integrately

No-code integration platform for connecting Loops to other tools.

Bubble

Send transactional email and sync contacts from Bubble no-code apps.

Framer

Capture form submissions in Framer sites and add them to Loops.

Webflow

Capture Webflow form submissions and add them to Loops contacts.

Emailify

Import custom MJML email templates from Emailify into Loops.

Email Love

Import MJML email templates from Email Love into Loops.

Incoming Webhooks

Receive arbitrary HTTP payloads and turn them into Loops events.

Resources

🌐
Portal
Portal
🔗
Documentation
Documentation
🔗
Documentation
Documentation
🔗
OpenAPI
OpenAPI
🔗
OpenAPI
OpenAPI
🚀
GettingStarted
GettingStarted
🔗
Webhooks
Webhooks
💰
Pricing
Pricing
📰
Blog
Blog
📄
ChangeLog
ChangeLog
📜
TermsOfService
TermsOfService
📜
PrivacyPolicy
PrivacyPolicy
🔗
Security
Security
📝
Signup
Signup
🔗
Login
Login
🔗
Twitter
Twitter
🔗
LinkedIn
LinkedIn
👥
GitHubOrganization
GitHubOrganization
📦
SDKs
SDKs
📦
SDKs
SDKs
📦
SDKs
SDKs
📦
SDKs
SDKs
📦
SDKs
SDKs
🔧
Tools
Tools
🔧
Tools
Tools
🔧
Tools
Tools
💻
CodeExamples
CodeExamples
🔗
Package
Package
🔗
Plans
Plans

Sources

Raw ↑
opencollection: 1.0.0
info:
  name: Loops OpenAPI Spec
  version: 1.8.0
items:
- info:
    name: API key
    type: folder
  items:
  - info:
      name: Test your API key
      type: http
    http:
      method: GET
      url: https://app.loops.so/api/v1/api-key
      auth:
        type: bearer
        token: '{{bearerToken}}'
    docs: Test your API key
- info:
    name: Contacts
    type: folder
  items:
  - info:
      name: Create a contact
      type: http
    http:
      method: POST
      url: https://app.loops.so/api/v1/contacts/create
      body:
        type: json
        data: '{}'
      auth:
        type: bearer
        token: '{{bearerToken}}'
    docs: Add a contact to your audience.
  - info:
      name: Update a contact
      type: http
    http:
      method: PUT
      url: https://app.loops.so/api/v1/contacts/update
      body:
        type: json
        data: '{}'
      auth:
        type: bearer
        token: '{{bearerToken}}'
    docs: Update a contact by `email` or `userId`. You must provide one of these parameters.<br>If you want to update a contact’s
      email address, the contact will first need a `userId` value. You can then make a request containing the userId field
      along with an updated email address.
  - info:
      name: Find a contact
      type: http
    http:
      method: GET
      url: https://app.loops.so/api/v1/contacts/find
      params:
      - name: email
        value: ''
        type: query
        description: Email address (URI-encoded)
      - name: userId
        value: ''
        type: query
      auth:
        type: bearer
        token: '{{bearerToken}}'
    docs: Search for a contact by `email` or `userId`. Only one parameter is allowed.
  - info:
      name: Delete a contact
      type: http
    http:
      method: POST
      url: https://app.loops.so/api/v1/contacts/delete
      body:
        type: json
        data: '{}'
      auth:
        type: bearer
        token: '{{bearerToken}}'
    docs: Delete a contact by `email` or `userId`.
  - info:
      name: Get suppression status for a contact
      type: http
    http:
      method: GET
      url: https://app.loops.so/api/v1/contacts/suppression
      params:
      - name: email
        value: ''
        type: query
        description: Email address (URI-encoded)
      - name: userId
        value: ''
        type: query
      auth:
        type: bearer
        token: '{{bearerToken}}'
    docs: Retrieve suppression status and removal quota for a contact by `email` or `userId`. Include only one query parameter.
  - info:
      name: Remove a contact from suppression list
      type: http
    http:
      method: DELETE
      url: https://app.loops.so/api/v1/contacts/suppression
      params:
      - name: email
        value: ''
        type: query
        description: Email address (URI-encoded)
      - name: userId
        value: ''
        type: query
      auth:
        type: bearer
        token: '{{bearerToken}}'
    docs: Remove a suppressed contact from the suppression list by `email` or `userId`. Include only one query parameter.
- info:
    name: Contact properties
    type: folder
  items:
  - info:
      name: Get a list of contact properties
      type: http
    http:
      method: GET
      url: https://app.loops.so/api/v1/contacts/properties
      params:
      - name: list
        value: ''
        type: query
        description: \"all\" (default) or \"custom\"
      auth:
        type: bearer
        token: '{{bearerToken}}'
    docs: Retrieve a list of your account's contact properties.<br>Use the `list` parameter to query "all" or "custom" properties.
  - info:
      name: Create a contact property
      type: http
    http:
      method: POST
      url: https://app.loops.so/api/v1/contacts/properties
      body:
        type: json
        data: '{}'
      auth:
        type: bearer
        token: '{{bearerToken}}'
    docs: Add a contact property to your team.
- info:
    name: Dedicated sending IPs
    type: folder
  items:
  - info:
      name: Get dedicated sending IP addresses
      type: http
    http:
      method: GET
      url: https://app.loops.so/api/v1/dedicated-sending-ips
      auth:
        type: bearer
        token: '{{bearerToken}}'
    docs: Retrieve a list of Loops' dedicated sending IP addresses.
- info:
    name: Mailing lists
    type: folder
  items:
  - info:
      name: Get a list of mailing lists
      type: http
    http:
      method: GET
      url: https://app.loops.so/api/v1/lists
      auth:
        type: bearer
        token: '{{bearerToken}}'
    docs: Retrieve a list of your account's mailing lists.
- info:
    name: Events
    type: folder
  items:
  - info:
      name: Send an event
      type: http
    http:
      method: POST
      url: https://app.loops.so/api/v1/events/send
      headers:
      - name: Idempotency-Key
        value: ''
      body:
        type: json
        data: '{}'
      auth:
        type: bearer
        token: '{{bearerToken}}'
    docs: Send events to trigger emails in Loops.
- info:
    name: Transactional emails
    type: folder
  items:
  - info:
      name: List transactional emails
      type: http
    http:
      method: GET
      url: https://app.loops.so/api/v1/transactional
      params:
      - name: perPage
        value: ''
        type: query
        description: How many results to return in each request. Must be between 10 and 50. Default is 20.
      - name: cursor
        value: ''
        type: query
        description: A cursor, to return a specific page of results. Cursors can be found from the `pagination.nextCursor`
          value in each response.
      auth:
        type: bearer
        token: '{{bearerToken}}'
    docs: Get a list of published transactional emails.
  - info:
      name: Send a transactional email
      type: http
    http:
      method: POST
      url: https://app.loops.so/api/v1/transactional
      headers:
      - name: Idempotency-Key
        value: ''
      body:
        type: json
        data: '{}'
      auth:
        type: bearer
        token: '{{bearerToken}}'
    docs: Send a transactional email to a contact.<br>Please [email us](mailto:help@loops.so) to enable attachments on your
      account before using them with the API.
- info:
    name: Themes
    type: folder
  items:
  - info:
      name: Get a theme
      type: http
    http:
      method: GET
      url: https://app.loops.so/api/v1/themes/:themeId
      params:
      - name: themeId
        value: ''
        type: path
        description: The ID of the theme.
      auth:
        type: bearer
        token: '{{bearerToken}}'
    docs: Retrieve a single theme by ID.
  - info:
      name: List themes
      type: http
    http:
      method: GET
      url: https://app.loops.so/api/v1/themes
      params:
      - name: perPage
        value: ''
        type: query
        description: How many results to return in each request. Must be between 10 and 50. Default is 20.
      - name: cursor
        value: ''
        type: query
        description: A cursor to return a specific page of results. Cursors can be found from the `pagination.nextCursor`
          value in each response.
      auth:
        type: bearer
        token: '{{bearerToken}}'
    docs: Retrieve a paginated list of email themes, most recently created first.
- info:
    name: Components
    type: folder
  items:
  - info:
      name: Get a component
      type: http
    http:
      method: GET
      url: https://app.loops.so/api/v1/components/:componentId
      params:
      - name: componentId
        value: ''
        type: path
        description: The ID of the component.
      auth:
        type: bearer
        token: '{{bearerToken}}'
    docs: Retrieve a single component by ID.
  - info:
      name: List components
      type: http
    http:
      method: GET
      url: https://app.loops.so/api/v1/components
      params:
      - name: perPage
        value: ''
        type: query
        description: How many results to return in each request. Must be between 10 and 50. Default is 20.
      - name: cursor
        value: ''
        type: query
        description: A cursor to return a specific page of results. Cursors can be found from the `pagination.nextCursor`
          value in each response.
      auth:
        type: bearer
        token: '{{bearerToken}}'
    docs: Retrieve a paginated list of email components.
- info:
    name: Campaigns
    type: folder
  items:
  - info:
      name: List campaigns
      type: http
    http:
      method: GET
      url: https://app.loops.so/api/v1/campaigns
      params:
      - name: perPage
        value: ''
        type: query
        description: How many results to return in each request. Must be between 10 and 50. Default is 20.
      - name: cursor
        value: ''
        type: query
        description: A cursor to return a specific page of results. Cursors can be found from the `pagination.nextCursor`
          value in each response.
      auth:
        type: bearer
        token: '{{bearerToken}}'
    docs: Retrieve a paginated list of campaigns.
  - info:
      name: Create a campaign
      type: http
    http:
      method: POST
      url: https://app.loops.so/api/v1/campaigns
      body:
        type: json
        data: '{}'
      auth:
        type: bearer
        token: '{{bearerToken}}'
    docs: Create a new draft campaign. An empty email message is created automatically and its `emailMessageId` is returned.
      Use the `/email-messages/{emailMessageId}` endpoint to set subject, sender, preview text, and LMX content.
  - info:
      name: Get a campaign
      type: http
    http:
      method: GET
      url: https://app.loops.so/api/v1/campaigns/:campaignId
      params:
      - name: campaignId
        value: ''
        type: path
        description: The ID of the campaign.
      auth:
        type: bearer
        token: '{{bearerToken}}'
    docs: Retrieve a single campaign by ID.
  - info:
      name: Update a campaign
      type: http
    http:
      method: POST
      url: https://app.loops.so/api/v1/campaigns/:campaignId
      params:
      - name: campaignId
        value: ''
        type: path
        description: The ID of the campaign.
      body:
        type: json
        data: '{}'
      auth:
        type: bearer
        token: '{{bearerToken}}'
    docs: Update a draft campaign's name. Campaigns can only be updated while in draft status.
- info:
    name: Email messages
    type: folder
  items:
  - info:
      name: Get an email message
      type: http
    http:
      method: GET
      url: https://app.loops.so/api/v1/email-messages/:emailMessageId
      params:
      - name: emailMessageId
        value: ''
        type: path
        description: The ID of the email message.
      auth:
        type: bearer
        token: '{{bearerToken}}'
    docs: Retrieve an email message, including its compiled LMX content.
  - info:
      name: Update an email message
      type: http
    http:
      method: POST
      url: https://app.loops.so/api/v1/email-messages/:emailMessageId
      params:
      - name: emailMessageId
        value: ''
        type: path
        description: The ID of the email message.
      body:
        type: json
        data: '{}'
      auth:
        type: bearer
        token: '{{bearerToken}}'
    docs: Update fields on an email message (subject, preview text, sender, LMX content). The campaign must be in draft status.
      Supply `expectedRevisionId` matching the current `contentRevisionId` — the server rejects mismatched revisions with
      409.
- info:
    name: Uploads
    type: folder
  items:
  - info:
      name: Create an upload
      type: http
    http:
      method: POST
      url: https://app.loops.so/api/v1/uploads
      body:
        type: json
        data: '{}'
      auth:
        type: bearer
        token: '{{bearerToken}}'
    docs: Request a pre-signed URL to upload an image asset. Upload the file with an HTTP `PUT` to the returned `presignedUrl`
      (sending the same `Content-Type` and `Content-Length`), then call `/uploads/{id}/complete` to finalize the asset.
  - info:
      name: Complete an upload
      type: http
    http:
      method: POST
      url: https://app.loops.so/api/v1/uploads/:id/complete
      params:
      - name: id
        value: ''
        type: path
        description: The `emailAssetId` returned when the upload was created.
      auth:
        type: bearer
        token: '{{bearerToken}}'
    docs: Finalize an asset after the file has been uploaded to the pre-signed URL. Returns the public URL of the uploaded
      asset.
bundled: true