# Emailit Docs (full text) # Connected apps > Review the AI assistants and apps you connected to Emailit with OAuth, change which workspaces each one can use, and revoke access. **Connected apps** lists every AI assistant and app you allowed to use Emailit through OAuth, such as ChatGPT, Claude, Cursor, Codex, Grok or an integration you approved. Each app works only in the workspaces you chose, with your role in each one. Open it from the account menu: **Account → Connected apps**. Connected apps belong to your account, not to a workspace: the page shows your connections across all your workspaces. Apps that use an API key instead of OAuth don't appear here; manage those keys under **Email API → API Keys**. ## The list | Column | Shows | | --- | --- | | **App** | The app's name and logo, or the host of its client metadata document | | **Workspaces** | **All workspaces**, or the workspaces you selected | | **Access** | **Full access** or **Sending only** (the approved scope) | | **Connected** | When you approved the app | | **Status** | **Active** or **Revoked** | Search by app or workspace name, filter the list and sort by app name or connection date. When nothing is connected yet, the page links to the [MCP server setup](/docs/mcp/). ## Change an app's workspaces 1. **Open Edit access.** Select **Edit access** next to the app. 2. **Choose workspaces.** Pick **All workspaces** (every workspace you belong to, including ones you create or join later) or **Only selected workspaces** and tick the ones you want. At least one is required. 3. **Choose where it starts.** **Start in** sets the workspace the app uses when you don't name one. 4. **Save.** Select **Save access**. Changes apply to the app's next request, without reconnecting. In workspaces where you're a Member, the app can send and read, but can't manage API keys or delete domains. Suspended workspaces are marked in the list. ## Revoke access Select **Revoke access** next to the app and confirm. The app loses access to its workspaces immediately: its tokens stop working and it has to ask for permission again to reconnect. > **Caution:** Removing Emailit from the app itself, for example deleting the connector in Claude or running `codex mcp logout`, doesn't revoke its access in Emailit. Revoke it here. ## When access changes on its own - **You leave a workspace**, or someone removes you: every app loses access to that workspace. - **A refresh token is reused**: Emailit revokes the connection as a precaution, and the app shows as **Revoked**. Connect it again from the app. - **60 days without use**: the app's refresh token expires and it has to connect again. ## Related - [Workspaces and permissions](/docs/mcp/workspaces-and-permissions/): How workspace access, roles and scopes combine. - [Manage grants with the API](/docs/developers/oauth-apps/#manage-grants): List, edit and revoke grants from code. --- Source: https://emailit.com/docs/account/connected-apps/ --- # Your Emailit account > How your personal Emailit account differs from a workspace, what you can change on the Account page, and how sign-up, timezone and support work. Your account is you: your name, email address, password and the ways you sign in. Everything you send, from domains to API keys and invoices, lives in a workspace that your account belongs to. This page explains how the two fit together and where to manage your own settings. ## Account vs workspace | | Account | Workspace | | --- | --- | --- | | Belongs to | One person | A team, product, brand or client | | Holds | Name, email, password, two-factor authentication, passkeys, referral code | Domains, API keys, emails, contacts, templates, webhooks, settings, members | | Billing | None | Plan, credits, payment method and invoices | | Access | Signs in to the dashboard | Each member has a role: Admin or Member | One account can belong to any number of workspaces, with a different role in each. You switch between them with the workspace switcher at the top of the sidebar. See [Workspaces](/docs/workspaces/). ## The Account page Open the account menu at the bottom of the sidebar and select **Account**. The page has two tabs: - **General** — rename yourself, change your email or password, and manage two-factor authentication and passkeys. - **Referrals** — your referral code, share link and rewards. See [Refer a friend](/docs/programs/referrals/). ## What you can change | Setting | Where | Details | | --- | --- | --- | | Name | **General** > **Rename** | Used when Emailit addresses you, for example in sign-in emails. | | Email address | **General** > **Change email** | Confirm with a 6-digit code sent to the new address. See [Sign-in and security](/docs/account/sign-in-and-security/). | | Password | **General** > **Change password** | At least 8 characters. Needs your current password. | | Two-factor authentication | **General** > **Two-Factor Authentication** | Use an authenticator app instead of emailed codes. See [Two-factor authentication and passkeys](/docs/account/two-factor-and-passkeys/). | | Passkeys | **General** > **Passkeys** | Sign in with Touch ID, Face ID or a security key. | | Timezone | Account menu > **Timezone** | **Standard (UTC)** or **Local**. Applies to dates shown in the dashboard. | The timezone choice is saved in your browser, so set it once on each device you use. It defaults to **Local**. Emailit doesn't offer self-service account deletion. To close your account, email [support@emailit.com](mailto:support@emailit.com) from the address on the account. ## Sign up 1. **Create your login.** Go to [dash.emailit.com/register](https://dash.emailit.com/register) and enter **Name**, **Email address**, **Password** (at least 8 characters) and **Confirm Password**. 2. **Add a code (optional).** If a friend or partner gave you a code, select **Do you have a Partner/Referral code?**, enter it and select **Apply**. If you arrived through a referral or partner link, the code is already filled in. Codes can only be added at sign-up. See [Refer a friend](/docs/programs/referrals/) and [Partner program](/docs/programs/partners/). 3. **Complete the security check.** Pass the Cloudflare Turnstile check and select **Sign up**. Signing up means you accept the Terms of Service, Privacy Policy and [Acceptable Use Policy](/acceptable-use-policy/). 4. **Verify your email.** Enter the 6-digit code from the email titled "Your Emailit verification code". Codes expire after 15 minutes. Select **Resend code** if it doesn't arrive. You can't open the dashboard until your email is verified. 5. **Set up your workspace.** On **Set up your workspace**, either create a workspace (enter a **Workspace name**, such as `Acme Inc.`) or join an existing one with an 8-character **Invite code** from a teammate. A new workspace starts on Pay as you go, with the free monthly credits included with Pay as you go, and in sandbox mode. In sandbox mode you can only send to the account emails of the workspace's members until Emailit approves [production access](/docs/workspaces/production-access/). If a teammate invited you by email, open the link in the invitation instead. On the invitation page, select **No account yet? Sign up** and register with the invited address. The invitation is accepted as soon as you finish. See [Invitations](/docs/workspaces/invitations/). ## Get support Select **Get support** in the top bar of the dashboard to see your options: - **Email** [support@emailit.com](mailto:support@emailit.com) for account, delivery or verification issues. The team replies within 24 hours. - **Discord** at [discord.emailit.com](https://discord.emailit.com) for general questions, feedback and community help. Don't post account details there. Check [status.emailit.com](https://status.emailit.com) for incidents. ## Next steps - [Sign-in and security](/docs/account/sign-in-and-security/): How sign-in codes, password resets and email changes work. - [Two-factor and passkeys](/docs/account/two-factor-and-passkeys/): Add an authenticator app or a passkey. - [Notification emails](/docs/account/notification-emails/): Every email Emailit sends you and who receives it. - [Workspaces](/docs/workspaces/): Members, roles, settings and production access. --- Source: https://emailit.com/docs/account/ --- # Notification emails > Every email Emailit sends you about your account, workspaces, domains, webhooks, sending health and billing, who receives each one and how to control it. Emailit sends a small set of system emails about your account and workspaces. This page lists each one, when it's sent, who gets it and whether you can turn it off. Use it to make sure the right people receive alerts, and to allowlist the sender. System emails come from `noreply@emailit.com`. Add it to your allowlist so codes and alerts don't land in spam. ## Account emails These go to the email address on your account. They're required for sign-in and can't be turned off. | Subject | When it's sent | Details | | --- | --- | --- | | Your Emailit verification code | You sign up, or ask for a new code | 6-digit code, valid for 15 minutes. | | Your Emailit sign-in code | Every password sign-in, unless an authenticator app is on | 6-digit code, valid for 15 minutes. Turn on an [authenticator app](/docs/account/two-factor-and-passkeys/) to stop these. | | Reset your Emailit password | You select **Forgot your password?** | Link valid for 60 minutes. | | Confirm your new Emailit email | You change your account email | Sent to the new address. Code valid for 15 minutes. | ## Workspace and domain emails | Subject | When it's sent | Who receives it | | --- | --- | --- | | You have been invited to join Acme on Emailit | An admin invites someone under **Workspace → Settings → Members** | The invited address. The link is valid for 7 days. | | DNS records for acme.com | Someone uses **Send to email** on a domain's DNS setup | The address they entered, for example your DNS administrator. | | Sending domain acme.com is no longer verified | The daily DNS check finds that a verified domain's SPF, DKIM or return-path record is broken | The workspace owner. | ## Webhook emails | Subject | When it's sent | Who receives it | | --- | --- | --- | | Webhook delivery is failing: Orders | A webhook request fails 3 times in a row | The workspace owner. | | Webhook disabled after delivery failures: Orders | An endpoint keeps failing for 3 days and Emailit disables it | The workspace owner. | See [Retries and failures](/docs/webhooks/retries-and-failures/) for the retry schedule and how to re-enable an endpoint. ## Sending health emails Emailit checks bounce rates every hour. When a threshold is crossed, the workspace owner gets one of these: | Subject | Meaning | | --- | --- | | Your Emailit sending health needs attention | Bounce rate is elevated. Clean your lists. | | Urgent: your Emailit sending health is at risk | Bounce rate is high and sending may be paused. | | Sending from acme.com is paused | One domain's bounce rate is too high, so that domain is paused. Other domains keep sending. | | Your Emailit workspace has been suspended | The workspace's bounce rate is too high. New mail won't send until support restores it. | See [Sending health](/docs/deliverability/sending-health/) for the exact thresholds. ## Billing emails Billing emails go to the **Notification emails** listed in the **Auto-refill** card under **Workspace → Billing**. If that list is empty, they go to the workspace's billing email. Only admins can change these settings. | Subject | When it's sent | Setting that controls it | | --- | --- | --- | | Emailit is about to auto-refill your credits | Auto-refill is about to charge your card | **Email when a refill is about to happen** | | Emailit auto-refilled your credits | Auto-refill charged your card and added credits | **Email when a refill happens** | | Emailit credits are running low | Your balance drops to the **Running-out threshold**. At most once every 24 hours. | **Alert when running out of credits** | | New Emailit invoice | An invoice is paid, for example a subscription renewal | **Email the same addresses when a new invoice is issued**, also shown as **Receive PDF invoices** in the billing email settings | All four are on by default. See [Auto-refill and alerts](/docs/billing/auto-refill/) and [Invoices and billing details](/docs/billing/invoices/). ## Who is the workspace owner? The owner is the person who created the workspace. Owner alerts go to the owner's account email. The owner can't be removed from the workspace, and the dashboard has no way to transfer ownership, so contact [support@emailit.com](mailto:support@emailit.com) if alerts need to reach someone else. Until then, have the owner forward them or set up a mail rule. ## What isn't emailed - **Replies to workspace requests.** When the Emailit team replies to a production access, custom plan or dedicated IP request, the reply appears in the request's conversation under **Workspace → Settings → Requests**. Check there for updates. - **Individual bounces and complaints.** Emailit doesn't email you per message. Use [webhooks](/docs/webhooks/) or the **Notify me of any bounces** [automation](/docs/automations/recipes/) to get them. - **Notification preferences.** Apart from the billing toggles above, you can't unsubscribe from system emails. They're limited to security, delivery and billing events. ## Related - [Sign-in and security](/docs/account/sign-in-and-security/) - [Auto-refill and alerts](/docs/billing/auto-refill/) - [Sending health](/docs/deliverability/sending-health/) --- Source: https://emailit.com/docs/account/notification-emails/ --- # Sign-in and account security > How signing in to Emailit works, from emailed one-time codes to passkeys, plus how to reset your password, change your email and keep your account safe. This page covers how you sign in to the dashboard, what to do when you forget your password, and how to change your email, password and name. It applies to your personal account, not to API keys. ## How sign-in works Every password sign-in needs a second step. Which one depends on your setup: | Your setup | Step 1 | Step 2 | | --- | --- | --- | | Default | Email and password | A 6-digit code emailed to you, every time | | Authenticator app enabled | Email and password | A code from your authenticator app, or a recovery code. No email code is sent. | | Passkey | Choose the passkey | None. The passkey signs you in on its own. | 1. **Enter your credentials.** Go to [dash.emailit.com/login](https://dash.emailit.com/login) and enter your email address and password, then select **Sign in**. 2. **Enter the second factor.** If you use emailed codes, check your inbox for "Your Emailit sign-in code" and enter the 6 digits on **Check your email**. The code expires after 15 minutes. Select **Resend code** to get a new one. If you set up an authenticator app, enter its 6-digit code instead, or select **Use recovery code**. To sign in with a passkey, select **Sign in with a passkey**, or pick your passkey from your browser's autofill suggestions in the email field. After 5 wrong codes, sign-in is locked for 15 minutes and you see "Too many attempts. Please try again later." > **Didn't try to sign in?:** If you get a sign-in code you didn't request, someone has your password. [Reset your password](#reset-your-password) right away and turn on [two-factor authentication](/docs/account/two-factor-and-passkeys/). ## Reset your password 1. **Request a link.** On the sign-in page, select **Forgot your password?**, enter your account email and select **Email reset password link**. 2. **Open the email.** Look for "Reset your Emailit password". For privacy, the page says a link was sent even if no account uses that address. 3. **Choose a new password.** Select **Reset password** in the email and set a new password of at least 8 characters. The link expires after 60 minutes and works once. Requesting a new link cancels any earlier one. A password reset doesn't turn off your authenticator app, so you still need it at the next sign-in. ## Change your email address 1. **Open your account.** In the account menu at the bottom of the sidebar, select **Account**. 2. **Enter the new address.** Under **Change email**, type the **New email** and select **Send verification code**. 3. **Confirm the code.** Enter the 6-digit code sent to the new address and select **Confirm email**. Your sign-in email doesn't change until you confirm. You can't switch to an address that another Emailit account already uses. Changing your account email doesn't change the billing email of any workspace. Update that in [Invoices and billing details](/docs/billing/invoices/). Pending workspace invitations are tied to the address they were sent to, so accept them before you change your email, or ask for a new invitation. ## Change your password Under **Change password**, enter your **Current password**, a **New password** of at least 8 characters, and **Confirm new password**, then select **Change password**. ## Rename your account Under **Rename**, edit **Name** and select **Rename**. Emailit uses this name when it emails you. ## Delete your account Account deletion isn't self-service. Email [support@emailit.com](mailto:support@emailit.com) from the address on your account. If you own workspaces, tell us what should happen to them. To delete stored email data, see [Data retention](/docs/data-retention/). ## Security recommendations - **Turn on an authenticator app or add a passkey.** Both stop someone with only your password from reading your sign-in code in a compromised inbox. See [Two-factor authentication and passkeys](/docs/account/two-factor-and-passkeys/). - **Use a unique password.** Use a password manager and don't reuse your email password. - **Store recovery codes offline.** They're your way back in if you lose your phone. - **Give teammates their own accounts.** Invite each person to the workspace instead of sharing a login. See [Members and roles](/docs/workspaces/members-and-roles/). - **Use scoped API keys for code.** Apps and agents should use a sending-only key restricted to one domain, never your dashboard login. See [API keys](/docs/developers/api-keys/). - **Remove people who leave.** An admin can remove a member under **Workspace → Settings → Members**. ## Related - [Two-factor authentication and passkeys](/docs/account/two-factor-and-passkeys/) - [Notification emails](/docs/account/notification-emails/) - [Security and compliance](/docs/security/) --- Source: https://emailit.com/docs/account/sign-in-and-security/ --- # Two-factor authentication and passkeys > Turn on an authenticator app with recovery codes, add Touch ID, Face ID or security-key passkeys, and regain access if you lose your device. Emailit always asks for a second factor when you sign in with a password. By default that's a code sent to your email. This page shows how to replace it with an authenticator app, how to add passkeys, and what to do if you lose a device. ## Before you begin - You're signed in to the dashboard. - For an authenticator app: an app such as 1Password, Google Authenticator, Microsoft Authenticator or Authy. - For passkeys: a browser and device that support passkeys (WebAuthn), such as Touch ID, Face ID, Windows Hello or a hardware security key. The **Add passkey** button only appears in browsers that support them. ## Turn on an authenticator app 1. **Open your account.** In the account menu at the bottom of the sidebar, select **Account**. Find **Two-Factor Authentication**, which shows **Two-factor authentication is disabled**. 2. **Start setup.** Select **Enable**. The **Set up Two-Factor Authentication** dialog shows a QR code and a **Secret key**. 3. **Add Emailit to your app.** Scan the QR code, or copy the **Secret key** into your app if you can't scan. 4. **Confirm.** Enter the 6-digit code from your app under **Verification Code** and select **Enable**. 5. **Save your recovery codes.** Emailit shows 8 recovery codes once. Store them in a password manager or print them, then select **Done**. From now on, sign-in asks for a code from your app instead of an emailed code. Emailit stops sending sign-in codes by email while the authenticator app is on. ### Recovery codes Each recovery code works once. At the sign-in prompt, select **Use recovery code** and enter one of them. The dashboard shows your recovery codes only right after you enable two-factor authentication. To get a fresh set, turn two-factor authentication off and on again. That also creates a new secret, so remove the old Emailit entry from your authenticator app and scan the new QR code. ## Turn off the authenticator app Under **Two-Factor Authentication**, select **Disable**, enter your password and select **Disable**. Sign-in goes back to a 6-digit code emailed to you. Your secret and recovery codes are deleted. ## Add a passkey A passkey lets you sign in without a password or code. It's tied to your device or security key, which makes it resistant to phishing. 1. **Open Passkeys.** On the **Account** page, find **Passkeys** and select **Add passkey**. 2. **Name it.** Enter a **Device name** you'll recognize later, such as `MacBook Pro` or `YubiKey 5C`, and select **Continue**. 3. **Follow your browser's prompt.** Confirm with Touch ID, Face ID, your device PIN or by touching your security key. The passkey appears in the list with when it was created and last used. You can add several, for example one per laptop plus a hardware key as a backup. To sign in with it, select **Sign in with a passkey** on the sign-in page, or choose the passkey from your browser's autofill in the email field. ### Delete a passkey Select the trash icon next to the passkey and confirm. It can no longer be used to sign in. This can't be undone, but you can add the same device again. ## Two-factor status of your teammates The member list under **Workspace → Settings → Members** has a **2FA** column. **Enabled** means the member signs in with an authenticator app, and **Disabled** means they use emailed codes. Pending invitations show **Pending**, and **Unknown** means the status couldn't be loaded. Emailit can't require two-factor authentication for a workspace, so ask your team to turn it on. ## If you lose your device 1. **Use a recovery code.** On the authenticator prompt, select **Use recovery code** and enter one of your saved codes. 2. **Use another method.** If you added a passkey on another device or a security key, sign in with **Sign in with a passkey** instead. 3. **Reset two-factor authentication.** Once you're in, disable two-factor authentication under **Account**, then enable it again with your new device. You get a new QR code and new recovery codes. 4. **Contact support.** If you have no recovery codes and no passkey, email [support@emailit.com](mailto:support@emailit.com) from your account address and explain what happened. Support needs to confirm you own the account before removing two-factor authentication. ## Related - [Sign-in and security](/docs/account/sign-in-and-security/) - [Members and roles](/docs/workspaces/members-and-roles/) - [Security and compliance](/docs/security/) --- Source: https://emailit.com/docs/account/two-factor-and-passkeys/ --- # Advanced analytics > Read the seven Advanced analytics charts, from SMTP response codes to time to inbox and delivery by provider, and use them to diagnose delivery problems. The **Advanced** tab of **Email API → Analytics** is for working out why delivery changed, not only that it changed. Its charts break delivery down by final status, by the reply codes receiving servers sent, by speed and by mailbox provider. It's available on Pro, Business, Custom. The tab uses the same **Sending domain**, **API key** and date range controls as the rest of Analytics, and you can rearrange, resize, add and remove its widgets the same way as on the [Dashboard tab](/docs/analytics/dashboard/). Every widget's **View details** opens a larger chart with a breakdown table. ## The charts ### Emails by status A stacked chart of outgoing emails created in each period, by the status they ended with: **Delivered**, **Bounced**, **Failed**, **Rejected**, **Suppressed**, **Complained** and **Canceled**. Emails that were opened or clicked count as Delivered on the chart; **View details** lists **Loaded** and **Clicked** separately. Emails still in progress (accepted, scheduled or attempted) and held emails aren't shown, so the most recent periods can look smaller until delivery finishes. ### Delivery response statuses A bar chart of the SMTP reply codes from delivery attempts, for example `250` for accepted, `421` or `451` for "try later", and `550` for a rejected mailbox. The eight most common codes are shown, and the rest are grouped as **Other**. Each delivery attempt counts once, so one email that was retried three times adds three codes. ### 4xx and 5xx delivery errors A stacked chart of delivery attempts that got an error code: **4xx** (temporary, Emailit retries) and **5xx** (permanent, the email bounces). A rise in 4xx means receiving servers are deferring you; a rise in 5xx means they're refusing you. ### Time to inbox Three lines for successful deliveries: **Average**, **p50** (median) and **p95**. It measures how long the successful delivery attempt took, from starting the SMTP conversation to the receiving server accepting the message. Time spent waiting in the queue or between retries isn't included. **View details** shows how many deliveries fell into each duration bucket, from under 1 second to over 5 minutes. ### Loads and clicks Total opens and clicks over time, counting every load and click, by when it happened. It needs [tracking](/docs/tracking/) on your sending domains. Apple Mail Privacy Protection and similar features load images automatically, so treat loads as a trend rather than an exact count of readers. ### Failed, rejected, suppressed A stacked chart of emails that ended without a delivery attempt or with a processing error. **Suppressed** emails were sent to addresses on your suppression list, **Rejected** emails were blocked because the workspace wasn't verified, and **Failed** emails hit an error. See [Email statuses](/docs/logs/email-statuses/). ### Delivery by provider Delivery rate over time for each recipient mailbox provider: delivered, opened or clicked emails divided by emails sent. Providers are detected from the recipient's domain: | Provider | Domains | | --- | --- | | Gmail | `gmail.com`, `googlemail.com` | | Outlook | `outlook.com` | | Live | `live.com` | | Hotmail | `hotmail.com` | | Yahoo | `yahoo.com`, `ymail.com`, `rocketmail.com` and other `yahoo.*` domains | | iCloud | `icloud.com`, `me.com`, `mac.com` | | Other | Everything else | Business mailboxes hosted by Google Workspace or Microsoft 365 use their own domains, so they count as **Other**. **View details** lists sent, delivered, bounced, opened and clicked counts per provider. ## Diagnose common problems | What you see | What it usually means | What to do | | --- | --- | --- | | **Bounced** grows in Emails by status, with more `550` codes and a 5xx rise | You're sending to addresses that don't exist, for example from an old or purchased list. | Filter by **Sending domain** or **API key** to find the source, open **Email API → Emails** filtered by status `bounced` to see the replies, and clean the list. [Email verification](/docs/email-verification/) helps before large sends. | | 4xx errors and `421` or `451` codes rise, and p95 time to inbox climbs | A provider is throttling you, often after a sudden jump in volume or on a new domain or IP. | Check which provider drops in **Delivery by provider**. Spread sends over time and [warm up](/docs/deliverability/warm-up/) new domains. | | One provider's delivery rate drops while others are steady | That provider is filtering or rejecting you specifically. | Check SPF, DKIM and DMARC on the domain, read the replies on bounced emails to that provider, and see [Best practices](/docs/deliverability/best-practices/). | | **Suppressed** grows | Your code keeps sending to addresses that bounced or complained before. | Check suppressions before sending, or remove addresses from your own lists. See [Suppressions](/docs/suppressions/). | | **Rejected** appears | The workspace isn't verified and emails went to non-members. | Request [production access](/docs/workspaces/production-access/). | | Delivered is steady but loads and clicks fall | Messages may be landing in spam, or tracking stopped working. | Check that the [tracking domain](/docs/tracking/custom-tracking-domain/) is still verified and review [spam checks](/docs/logs/email-details/#spam-checks) on recent emails. | | Complaints rise | Recipients don't expect or want the mail. | Make unsubscribing easy, send only to people who opted in, and watch the Complaint rate card on the Dashboard tab. | When a chart points to a problem, go from the trend to individual emails: filter **Email API → Emails** by the same domain, status and dates, and read the **Deliveries** tab of a few affected emails. ## Related - [Queryable SQL](/docs/analytics/queryable/): Answer questions the charts don't cover. - [SMTP reply codes](/docs/dictionary/smtp-reply-codes/): What each code means. --- Source: https://emailit.com/docs/analytics/advanced/ --- # Analytics dashboard > Use the Analytics Dashboard tab to track bounce and complaint rates and build a widget grid of sends, bounces, opens, clicks and breakdowns. The **Dashboard** tab of **Email API → Analytics** is available on every plan. It shows your bounce and complaint rates and a grid of widgets that you can rearrange to fit what you watch every day. The layout is saved for the workspace, so everyone in it sees the same dashboard. ## Bounce rate and complaint rate Two cards sit at the top of the tab. They can't be removed. | Card | Shows | Reference lines | | --- | --- | --- | | **Bounce rate** | Bounced emails divided by sent emails in the selected range, with a trend line and the change from the previous period. | **Warning** at 3%, **Possible suspension** at 5%. | | **Complaint rate** | Spam complaints divided by sent emails. | **Warning** at 0.05%, **Possible suspension** at 0.3%. | If a rate is approaching the second line, act before your domain is paused: see [Sending health](/docs/deliverability/sending-health/) and [Bounces and complaints](/docs/deliverability/bounces-and-complaints/). ## Default widgets A new workspace starts with this layout: | Widget | Chart | Shows | | --- | --- | --- | | **Sends** | Timeseries, half width | Outgoing emails created. | | **Bounces** | Timeseries, half width | Emails whose status is bounced. | | **Complaints** | Timeseries, third width | Spam complaints. | | **Loads** | Timeseries, third width | Opens, counting every load. | | **Clicks** | Timeseries, third width | Tracked link clicks, counting every click. | | **Delivery by provider** | Bar, full width | Delivery rate for Gmail, Outlook, Live, Hotmail, Yahoo, iCloud and other providers. | ## Add a widget 1. **Open the dialog.** Select the **+** (**Add widget**) button next to the date range. 2. **Choose a metric.** Pick one from the list below. 3. **Choose a chart type.** **Timeseries**, **Stacked**, **Bar**, **Pie**, **Stat** or **Table**. If the metric doesn't support the type you pick, the widget uses the metric's default type. 4. **Add it.** Select **Add**. The widget is placed in the first free spot at half width. | Metric | Chart types | What it measures | | --- | --- | --- | | **Sends** | Timeseries, Stat | Outgoing emails created. | | **Bounces**, **Failed**, **Rejected**, **Suppressed** | Timeseries, Stat | Emails whose latest status is that status. | | **Complaints** | Timeseries, Stat | Spam complaints received. | | **Loads**, **Clicks** | Timeseries, Stat | Every open or click. | | **Unique loads**, **Unique clicks** | Timeseries, Stat | Emails opened or clicked at least once. | | **Unique emails** | Timeseries, Stat | Distinct emails in the range. | | **Avg delivery time** | Timeseries, Stat | Average duration of delivery attempts. | | **Delivery status** | Table, Pie, Timeseries | Emails by their latest status. | | **Top countries** | Table, Pie | Countries where emails were opened, top 10. | | **Top links** | Table | Most-clicked links by link ID, top 10. | | **By domain** | Table, Pie | Sends per sending domain. | | **By API key** | Table, Pie | Sends per API key. | | **By campaign** | Table, Pie | Sends per campaign. | | **By tag** | Table, Pie | Sends per tag. Usually a single "(none)" row, because API and SMTP mail isn't tagged. | | **Delivery by provider** | Bar, Table, Timeseries, Pie | Delivered emails divided by sent emails, per recipient mailbox provider. | Loads and clicks need tracking turned on for the sending domain. See [Tracking](/docs/tracking/). ## Arrange and size widgets - **Move:** drag a widget by its body. Other widgets move out of the way. - **Resize:** drag the handle in the bottom-right corner. Widths snap to a quarter, a third, half or the full width of the grid (3, 4, 6 or 12 of 12 columns), and heights to 1, 2 or 3 rows. - **Remove:** open the widget's menu (**…**) and select **Remove**. Changes are saved automatically for the whole workspace. ## Widget menu Each widget's **…** menu has: - **View details:** a larger chart with a breakdown table, for example the per-provider sent, delivered, bounced, opened and clicked counts for **Delivery by provider**. - **Refresh:** reloads only this widget. - **Range:** shows this widget for a different preset than the page, or **Page range** to follow the page again. The override lasts until you leave the page. - **Remove** ## Filters The **Sending domain** and **API key** filters and the date range at the top of the page apply to every widget and to the rate cards. See [Analytics](/docs/analytics/#filters-and-time-range) for presets, time zones and retention. ## Related - [Advanced analytics](/docs/analytics/advanced/) - [Sending health](/docs/deliverability/sending-health/) --- Source: https://emailit.com/docs/analytics/dashboard/ --- # Analytics > Track sends, bounces, complaints, opens and clicks over time with a customizable dashboard, advanced delivery charts, and SQL queries on higher plans. **Email API → Analytics** turns your sending activity into charts and totals, so you can see volume, delivery and engagement at a glance and investigate when something changes. It covers outgoing email: API, SMTP, campaign and automation sends. Received mail isn't included. ## Three levels | Tab | Plans | What it gives you | | --- | --- | --- | | **Dashboard** | All plans | Bounce rate and complaint rate cards, plus a grid of widgets you can add, resize and rearrange. See [Analytics dashboard](/docs/analytics/dashboard/). | | **Advanced** | Pro, Business, Custom | Seven delivery charts: emails by status, SMTP response codes, 4xx and 5xx errors, time to inbox, loads and clicks, non-delivery, and delivery by provider. See [Advanced analytics](/docs/analytics/advanced/). | | **Queryable** | Business, Custom | A query builder and raw ClickHouse SQL over your sending data, with saved queries. See [Queryable SQL](/docs/analytics/queryable/). | Tabs your plan doesn't include show a **Pro** or **Business** badge and can't be opened. ## Filters and time range The controls at the top of the page apply to every widget: - **Sending domain** and **API key:** narrow the data to one domain or one key. - **Date range:** presets **Last 30 minutes**, **Last 1 hour**, **Last 6 hours**, **Last 12 hours**, **Last 24 hours** (the default), **Last 7 days** and **Last 30 days**, or a custom range with start and end times. Custom ranges can span up to about 13 months. - **Time zone:** in the date picker, choose **UTC** or your local time zone. This changes how times are shown; data is stored in UTC. - **Refresh:** reloads every widget. Charts pick their resolution from the range: 5-minute buckets up to 6 hours, hourly buckets up to 48 hours, and daily buckets beyond that. Each widget also shows the change compared with the previous period of the same length, for example the 7 days before the last 7 days. ## Retention How far back you can look depends on your plan. If you pick a range older than your analytics retention, Emailit shortens it to the period it still has. | | Pay as you go | Pro | Business | Custom | | --- | --- | --- | --- | --- | | Analytics | Dashboard | Dashboard + Advanced | Dashboard + Advanced + Queryable (SQL) | Dashboard + Advanced + Queryable (SQL) | | Analytics kept | 365 days | Forever | Forever | Flexible | Detailed per-email data used by the Advanced charts and SQL tables is kept for up to 365 days; daily totals can be kept longer. See [Data retention](/docs/data-retention/). ## How analytics relates to other pages - Analytics shows trends. To see what happened to one email, use [Email logs](/docs/logs/). - [Sending health](/docs/deliverability/sending-health/) uses bounce rates among unique recipients to decide whether a domain or workspace is at risk. The bounce rate on the Analytics dashboard is a simpler bounces-per-send ratio, so the numbers can differ. - Analytics is updated continuously from your sending data, so the most recent activity can take a short while to appear. ## Get started - [Analytics dashboard](/docs/analytics/dashboard/) - [Advanced analytics](/docs/analytics/advanced/) - [Queryable SQL](/docs/analytics/queryable/) - [Plans](/pricing/) --- Source: https://emailit.com/docs/analytics/ --- # Queryable SQL > Query your sending data with the Queryable builder or raw ClickHouse SQL, learn the available tables and limits, and start from example queries. The **Queryable** tab of **Email API → Analytics** lets you ask your own questions of your sending data, either with a point-and-click builder or with read-only ClickHouse SQL. It's available on Business, Custom. Results show as a chart and a table, and you can save queries for your team. ## Builder The builder writes the query for you and uses the page's date range, **Sending domain** and **API key** filters. | Control | Options | | --- | --- | | **Measure** | `sends`, `bounces`, `complaints`, `loads`, `clicks`, `unique loads`, `unique clicks`, `unique emails`, `failed`, `rejected`, `suppressed`, `avg delivery time`, `bounce rate`, `complaint rate` | | **Time grain** | `hour`, `day`, `week` | | **Group by** | `none`, `status`, `domain`, `credential` (API key), `campaign`, `tag`, `country` | | **Chart type** | **Timeseries**, **Pie**, **Stat**, **Table** | Not every combination applies. **Group by** `country` works with loads and clicks, which have a country; `status`, `domain`, `credential`, `campaign` and `tag` work with the email measures (sends, bounces, failed, rejected, suppressed, unique emails and the two rates). Complaints and average delivery time aren't grouped. Groups show IDs such as `dom_…` and `key_…`; use raw SQL to join names. Grouping by `tag` usually returns a single `(none)` group. Select **Run** to see the result. ## Raw SQL Turn on **Raw SQL** to write ClickHouse SQL in the editor, then select **Run**. - Queries must start with `SELECT` or `WITH` and can only read the tables listed below. Writes, table functions such as `url()` or `s3()`, and `system` tables are blocked. - Every table is automatically limited to your workspace. You don't need a `workspace_oid` condition. - **The page's date range and filters don't apply to raw SQL.** Add your own time condition, such as `created_at >= now() - INTERVAL 30 DAY`, or queries scan all retained data. - Each `FROM` and `JOIN` must name a table directly. Subqueries in parentheses are fine, but don't put an alias after a table name (`FROM emails_raw e`) and don't select from a named `WITH` query. Qualify columns with the table name instead, for example `sending_domains_dim.name`. - Queries return at most 5,000 rows and stop after 10 seconds. A query can be up to 20,000 characters. To chart the result, return a time column named `t`, a numeric column named `v`, and optionally a `group_key` column for one series per value. Any other shape shows as a table. The table on the page shows the first 100 rows. ## Saved queries Type a name in **Save as…** and select **Save query** to store the current builder settings or SQL. Saved queries appear as buttons above the results for everyone in the workspace. Select one to load it, or select its **×** to delete it. ## Tables The `_raw` tables are kept for up to 365 days. The `_daily` tables hold daily totals and follow your plan's analytics retention. ### emails_raw One row per version of each email. Every status change adds a new version, so an email appears several times. Group by `oid` and use `argMax(column, _peerdb_version)` to read the latest value, as in the examples below. | Column | Description | | --- | --- | | `oid` | Email ID (`em_…`). | | `type` | `outgoing` for sent mail, `inbound` for received mail. | | `status` | The email status, for example `delivered` or `bounced`. | | `rcpt_to`, `mail_from`, `subject`, `message_id` | Envelope and subject. | | `sending_domain_oid`, `credential_oid` | Sending domain ID (`dom_…`) and API key ID (`key_…`). | | `campaign_oid`, `automation_oid` | Set for campaign and automation sends. | | `tag` | Usually empty. | | `spam_score`, `inspected` | Rspamd score, and whether the email was scored. | | `size` | Message size in bytes. | | `loaded`, `clicked` | Time of the first open and first click, or `NULL`. | | `created_at`, `updated_at` | Timestamps in UTC. | | `_peerdb_version` | Version number; higher is newer. | ### deliveries_raw One row per delivery record: each attempt, and records such as held or suppressed. | Column | Description | | --- | --- | | `oid`, `email_oid` | Delivery ID and the email it belongs to. | | `status` | For example `delivered`, `attempted`, `bounced`, `held`, `suppressed`. | | `output` | The receiving server's reply, for example `250 2.0.0 OK`. | | `details` | Emailit's description of the result. | | `time` | How long the attempt took, the value behind **Time to inbox**. | | `sent_with_ssl` | Whether the connection used TLS. | | `timestamp`, `created_at` | When the attempt happened. | ### loads_raw and clicks_raw One row per open or click. | Column | Description | | --- | --- | | `oid`, `email_oid` | Load or click ID and the email it belongs to. | | `link_oid` | `clicks_raw` only: the clicked link's ID (`link_…`). Link URLs aren't stored in the analytics tables. | | `ip_address`, `country`, `city`, `user_agent` | Where the open or click came from. | | `timestamp`, `created_at` | When it happened. | ### events_raw One row per [event](/docs/logs/events/): `oid` (`evt_…`), `type`, `data` (the payload as a JSON string; read it with functions such as `JSONExtractString(data, 'object', 'id')`) and `created_at`. ### Daily tables | Table | Columns | | --- | --- | | `events_daily` | `day`, `type`, `cnt`, `unique_emails` | | `deliveries_daily` | `day`, `status`, `cnt`, `avg_time` | | `loads_daily` | `day`, `cnt`, `unique_emails`, `countries` | | `clicks_daily` | `day`, `cnt`, `unique_emails`, `unique_links` | Sum `cnt` with `sum(cnt)`. The other columns are ClickHouse aggregate states: read them with `uniqMerge(unique_emails)`, `uniqMerge(unique_links)`, `avgMerge(avg_time)` and `topKMerge(10)(countries)`. ### Dimension tables | Table | Columns | | --- | --- | | `sending_domains_dim` | `oid`, `name`, `created_at` | | `campaigns_dim` | `oid`, `name`, `status`, `created_at` | | `workspaces_dim` | `oid`, `name`, `plan_id`, `created_at` | Join them to show names instead of IDs. ## Example queries ### Bounce rate by sending domain per day ```sql SELECT toDate(latest.first_seen) AS t, sending_domains_dim.name AS group_key, countIf(latest.status = 'bounced') / count() AS v FROM ( SELECT oid, min(created_at) AS first_seen, argMax(status, _peerdb_version) AS status, argMax(sending_domain_oid, _peerdb_version) AS sending_domain_oid FROM emails_raw WHERE type = 'outgoing' AND created_at >= now() - INTERVAL 30 DAY GROUP BY oid ) AS latest LEFT JOIN sending_domains_dim ON sending_domains_dim.oid = latest.sending_domain_oid GROUP BY t, group_key ORDER BY t, group_key ``` Choose the **Timeseries** chart to get one line per domain. ### Top clicked links ```sql SELECT link_oid, count() AS clicks, uniqExact(email_oid) AS emails_clicked FROM clicks_raw WHERE timestamp >= now() - INTERVAL 7 DAY GROUP BY link_oid ORDER BY clicks DESC LIMIT 20 ``` The analytics tables store the link ID, not the URL. To see a link's URL, open an email that has the click and check its **Clicks** tab, or collect `link.url` from `email.clicked` [webhooks](/docs/webhooks/event-types/). ### Delivery time percentiles ```sql SELECT toDate(timestamp) AS t, count() AS deliveries, quantile(0.5)(toFloat64(time)) AS p50, quantile(0.95)(toFloat64(time)) AS p95, quantile(0.99)(toFloat64(time)) AS p99 FROM deliveries_raw WHERE status = 'delivered' AND time IS NOT NULL AND timestamp >= now() - INTERVAL 14 DAY GROUP BY t ORDER BY t ``` ### Sends and opens per campaign ```sql SELECT campaigns_dim.name AS campaign, count() AS sent, countIf(latest.loaded IS NOT NULL) AS opened, countIf(latest.clicked IS NOT NULL) AS clicked FROM ( SELECT oid, argMax(campaign_oid, _peerdb_version) AS campaign_oid, argMax(loaded, _peerdb_version) AS loaded, argMax(clicked, _peerdb_version) AS clicked FROM emails_raw WHERE type = 'outgoing' AND created_at >= now() - INTERVAL 90 DAY GROUP BY oid ) AS latest INNER JOIN campaigns_dim ON campaigns_dim.oid = latest.campaign_oid GROUP BY campaign ORDER BY sent DESC ``` ## Errors | Message | Cause | | --- | --- | | Only SELECT queries are allowed | The query doesn't start with `SELECT` or `WITH`. | | Table "…" is not allowed | A `FROM` or `JOIN` names something that isn't an allowed table, such as a named `WITH` query. | | Query must read an allowlisted analytics table | The query has no `FROM` an allowed table. | | External tables and table functions are not allowed | The query uses a table function such as `url()`. | | Queries cannot target another workspace | A `workspace_oid = '…'` condition names a different workspace. | ClickHouse syntax errors, for example from an alias after a table name, are shown as returned by ClickHouse. ## Related - [Advanced analytics](/docs/analytics/advanced/) - [Events](/docs/logs/events/) --- Source: https://emailit.com/docs/analytics/queryable/ --- # Authentication > Authenticate API requests with a Bearer API key or OAuth access token, choose full or sending scope, restrict keys to a domain and handle auth errors. Every request to the Emailit API must carry a credential in the `Authorization` header. This page covers the two kinds of credentials (API keys and OAuth access tokens), what each scope can do, and every authentication error you can get back. ## API keys An API key belongs to one workspace, and every request made with it acts on that workspace. Keys look like this: ```text secret_Xq7pL2mN9vB4kR8tW1yZ6cH3jF5dS0aG ``` That's `secret_` followed by 32 letters and digits. Keys created before the `secret_` format have no prefix, and they keep working. Create keys in the dashboard under **Email API → API Keys**, or with [Create an API key](/docs/api-reference/api-keys/create/). The secret is shown only once, when you create or [regenerate](/docs/api-reference/api-keys/regenerate/) the key, so store it right away. See [API keys](/docs/developers/api-keys/) for managing them. The same keys work as the SMTP password for the [SMTP relay](/docs/smtp/settings/). ## Send the key with each request Use the `Bearer` scheme in the `Authorization` header. The API doesn't accept keys in the query string or request body. **cURL** ```bash curl https://api.emailit.com/v2/domains \ -H "Authorization: Bearer $EMAILIT_API_KEY" ``` **Node.js** ```javascript const response = await fetch('https://api.emailit.com/v2/domains', { headers: { Authorization: `Bearer ${process.env.EMAILIT_API_KEY}` }, }); const domains = await response.json(); ``` **Python** ```python import os import requests response = requests.get( "https://api.emailit.com/v2/domains", headers={"Authorization": f"Bearer {os.environ['EMAILIT_API_KEY']}"}, ) domains = response.json() ``` The SDKs set this header for you when you pass the key to the client. ## Scopes Each key has one of two scopes. You choose it when you create the key, and you can't change it later. | Scope | Can call | Use it for | | --- | --- | --- | | `full` | Every endpoint in the API. This is the default. | Back-office tools, scripts and integrations that manage domains, contacts, templates or webhooks. | | `sending` | Only the send endpoints listed below. | Application servers that only send email. | A `sending` key can call these endpoints and nothing else: | Endpoint | Description | | --- | --- | | `POST /emails` | [Send an email](/docs/api-reference/emails/send/) | | `POST /emails/{id}` | [Update a scheduled email](/docs/api-reference/emails/update/) | | `POST /emails/{id}/cancel` | [Cancel an email](/docs/api-reference/emails/cancel/) | | `POST /emails/{id}/retry` | [Retry an email](/docs/api-reference/emails/retry/) | | `POST /emails/{id}/forward` | [Forward an email](/docs/api-reference/emails/forward/) | Reading emails (list, retrieve, raw, body, metadata, attachments and status) needs a `full` key. When a `sending` key calls any other endpoint, the API returns `403` with `Permission denied: full` (or `Permission denied: read` for the email read endpoints). [All endpoints](/docs/api-reference/endpoints/) lists the scope of every endpoint. ## Restrict a key to one domain A `sending` key can also be locked to one sending domain. Pass the domain's ID as `sending_domain_id` when you [create the key](/docs/api-reference/api-keys/create/). A restricted key can only send from addresses on that domain. Any other `from` domain returns `403`: ```json { "error": "Domain not authorized", "message": "API key is not authorized to send from this domain" } ``` Domain restrictions apply to `sending` keys only. A `full` key always has access to every domain in the workspace. ## OAuth access tokens Apps that act on behalf of an Emailit user, such as MCP clients and third-party integrations, don't ask for an API key. They use OAuth 2.1 instead: the user signs in to Emailit, chooses the workspaces the app can use (all of them or only selected ones) and approves the `sending` or `full` scope, and the app receives an access token. The user can change or revoke that access under [Connected apps](/docs/account/connected-apps/). Send access tokens in the same header as API keys: ```http Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9… ``` An access token is valid for 15 minutes and acts on the grant's default workspace, with the granted scope and the user's role in that workspace. Apps refresh it with the refresh token. See [OAuth apps](/docs/developers/oauth-apps/) to build one. ## Authentication errors Authentication runs before anything else, so these errors can come back from any endpoint. | Status | `message` or `error` | Cause | Fix | | --- | --- | --- | --- | | `401` | `API key required` | The `Authorization` header is missing or doesn't start with `Bearer `. | Send `Authorization: Bearer `. | | `401` | `Valid API key required` | The header has the `Bearer` prefix but no token. | Check that the variable holding your key isn't empty. | | `401` | `Invalid API key` | The key doesn't exist, was deleted, or was regenerated (the old secret stops working), or an OAuth token expired. | Use a current key, or refresh the OAuth token. | | `403` | `Workspace is suspended` | The workspace is suspended. | Contact [support](/contact/). | | `403` | `Permission denied: full` | A `sending` key called an endpoint that needs `full`. | Use a `full` key. | | `403` | `Domain not authorized` | A domain-restricted key sent from another domain. | Send from the key's domain or use another key. | | `403` | `unverified_workspace_recipient` | The workspace isn't verified yet and a recipient isn't a workspace member. | See [Unverified workspaces](#unverified-workspaces). | | `503` | `Authentication service unavailable` | A temporary problem on our side. | Retry with backoff. | **401** ```json { "statusCode": 401, "error": "Unauthorized", "message": "Invalid API key" } ``` **403 Scope** ```json { "statusCode": 403, "error": "Forbidden", "message": "Permission denied: full" } ``` **403 Suspended** ```json { "statusCode": 403, "error": "Forbidden", "message": "Workspace is suspended" } ``` **403 Unverified** ```json { "code": "unverified_workspace_recipient", "error": "Workspace not verified", "message": "Unverified workspaces can only send to workspace members' account emails. Blocked recipient: ada@example.com.", "blocked_recipients": ["ada@example.com"] } ``` ## Unverified workspaces New workspaces start unverified. Until Emailit approves [production access](/docs/workspaces/production-access/), the API only sends to the account email addresses of the workspace's members. Sending, retrying or forwarding to anyone else returns `403` with the code `unverified_workspace_recipient` and the list of `blocked_recipients`, and campaigns can't be sent at all. Your API keys work normally for everything else. ## Keep your keys secret An API key gives access to your workspace, so treat it like a password. - Call the API from your server only. Never put a key in browser JavaScript, a mobile app or any other code that runs on someone else's device. - Keep keys out of source control. Load them from environment variables or a secret manager. - Create one key per application and environment, and name it after where it's used, so you can revoke one without breaking the others. - Give each key the least access it needs: a `sending` key, restricted to one domain, is enough for most applications. - Check `last_used_at` in [List API keys](/docs/api-reference/api-keys/list/) and delete keys you no longer use. - If a key leaks, [regenerate](/docs/api-reference/api-keys/regenerate/) it or [delete](/docs/api-reference/api-keys/delete/) it right away. The old secret stops working immediately. ## Related - [API keys](/docs/developers/api-keys/): Create, restrict and rotate keys in the dashboard. - [Errors](/docs/api-reference/errors/): Every error format and status code. - [OAuth apps](/docs/developers/oauth-apps/): Let users connect your app to their workspace. - [Production access](/docs/workspaces/production-access/): Get your workspace verified to send to anyone. --- Source: https://emailit.com/docs/api-reference/authentication/ --- # All endpoints > Every Emailit API v2 endpoint in one place, grouped by resource, with its HTTP method, path, the API key scope it needs and a link to its reference. This page lists every endpoint of the Emailit API, grouped by resource. Paths are relative to the base URL `https://api.emailit.com/v2`, and `{id}` stands for the ID of the object (or, where the endpoint allows it, its name or email address). The **Scope** column shows which [API keys](/docs/api-reference/authentication/#scopes) can call the endpoint: | Scope | Works with | | --- | --- | | `sending` | Keys with the `sending` or the `full` scope. | | `full` | Keys with the `full` scope only. | OAuth access tokens follow the same rules as API keys with the same scope. ## Emails Send email, look up messages and their content, and schedule, cancel, retry or forward them. See [Emails](/docs/api-reference/emails/) for the full reference. | Endpoint | Method | Path | Scope | | --- | --- | --- | --- | | [Send an email](/docs/api-reference/emails/send/) | `POST` | `/emails` | `sending` | | [List emails](/docs/api-reference/emails/list/) | `GET` | `/emails` | `full` | | [Retrieve an email](/docs/api-reference/emails/get/) | `GET` | `/emails/{id}` | `full` | | [Retrieve raw MIME](/docs/api-reference/emails/raw/) | `GET` | `/emails/{id}/raw` | `full` | | [List attachments](/docs/api-reference/emails/attachments/) | `GET` | `/emails/{id}/attachments` | `full` | | [Retrieve the body](/docs/api-reference/emails/body/) | `GET` | `/emails/{id}/body` | `full` | | [Retrieve metadata](/docs/api-reference/emails/meta/) | `GET` | `/emails/{id}/meta` | `full` | | [Update a scheduled email](/docs/api-reference/emails/update/) | `POST` | `/emails/{id}` | `sending` | | [Cancel an email](/docs/api-reference/emails/cancel/) | `POST` | `/emails/{id}/cancel` | `sending` | | [Retry an email](/docs/api-reference/emails/retry/) | `POST` | `/emails/{id}/retry` | `sending` | | [Forward an email](/docs/api-reference/emails/forward/) | `POST` | `/emails/{id}/forward` | `sending` | | [Retrieve status only](/docs/api-reference/emails/status/) | `GET` | `/email/{id}` | `full` | ## Domains Add sending domains, read their DNS records and verification status, and verify them. See [Domains](/docs/api-reference/domains/) for the full reference. | Endpoint | Method | Path | Scope | | --- | --- | --- | --- | | [Create a domain](/docs/api-reference/domains/create/) | `POST` | `/domains` | `full` | | [Retrieve a domain](/docs/api-reference/domains/get/) | `GET` | `/domains/{id}` | `full` | | [Verify a domain](/docs/api-reference/domains/verify/) | `POST` | `/domains/{id}/verify` | `full` | | [Update a domain](/docs/api-reference/domains/update/) | `POST` | `/domains/{id}` | `full` | | [List domains](/docs/api-reference/domains/list/) | `GET` | `/domains` | `full` | | [Delete a domain](/docs/api-reference/domains/delete/) | `DELETE` | `/domains/{id}` | `full` | ## DMARC reports Read aggregate and forensic DMARC reports for a sending domain, or upload your own. See [DMARC reports](/docs/api-reference/dmarc/) for the full reference. | Endpoint | Method | Path | Scope | | --- | --- | --- | --- | | [List aggregate reports](/docs/api-reference/dmarc/list/) | `GET` | `/domains/{id}/dmarc/reports` | `full` | | [Retrieve a report](/docs/api-reference/dmarc/get/) | `GET` | `/domains/{id}/dmarc/reports/{report_id}` | `full` | | [Upload a report](/docs/api-reference/dmarc/upload/) | `POST` | `/domains/{id}/dmarc/reports` | `full` | | [List forensic reports](/docs/api-reference/dmarc/forensic/) | `GET` | `/domains/{id}/dmarc/forensic` | `full` | | [Retrieve a forensic report](/docs/api-reference/dmarc/forensic-get/) | `GET` | `/domains/{id}/dmarc/forensic/{report_id}` | `full` | | [Retrieve statistics](/docs/api-reference/dmarc/stats/) | `GET` | `/domains/{id}/dmarc/stats` | `full` | | [List sending sources](/docs/api-reference/dmarc/sources/) | `GET` | `/domains/{id}/dmarc/sources` | `full` | | [List countries](/docs/api-reference/dmarc/countries/) | `GET` | `/domains/{id}/dmarc/countries` | `full` | | [List networks (ASNs)](/docs/api-reference/dmarc/asns/) | `GET` | `/domains/{id}/dmarc/asns` | `full` | | [List reporters](/docs/api-reference/dmarc/reporters/) | `GET` | `/domains/{id}/dmarc/reporters` | `full` | ## API keys Create, list, rename and revoke the API keys of a workspace. See [API keys](/docs/api-reference/api-keys/) for the full reference. | Endpoint | Method | Path | Scope | | --- | --- | --- | --- | | [Create an API key](/docs/api-reference/api-keys/create/) | `POST` | `/api-keys` | `full` | | [Retrieve an API key](/docs/api-reference/api-keys/get/) | `GET` | `/api-keys/{id}` | `full` | | [List API keys](/docs/api-reference/api-keys/list/) | `GET` | `/api-keys` | `full` | | [Update an API key](/docs/api-reference/api-keys/update/) | `POST` | `/api-keys/{id}` | `full` | | [Regenerate an API key](/docs/api-reference/api-keys/regenerate/) | `POST` | `/api-keys/{id}/regenerate` | `full` | | [Delete an API key](/docs/api-reference/api-keys/delete/) | `DELETE` | `/api-keys/{id}` | `full` | ## Audiences Manage subscriber lists used by campaigns and sign-up forms. See [Audiences](/docs/api-reference/audiences/) for the full reference. | Endpoint | Method | Path | Scope | | --- | --- | --- | --- | | [Create an audience](/docs/api-reference/audiences/create/) | `POST` | `/audiences` | `full` | | [Retrieve an audience](/docs/api-reference/audiences/get/) | `GET` | `/audiences/{id}` | `full` | | [Update an audience](/docs/api-reference/audiences/update/) | `POST` | `/audiences/{id}` | `full` | | [List audiences](/docs/api-reference/audiences/list/) | `GET` | `/audiences` | `full` | | [Delete an audience](/docs/api-reference/audiences/delete/) | `DELETE` | `/audiences/{id}` | `full` | ## Subscribers Add, update and remove the subscribers of an audience. See [Subscribers](/docs/api-reference/audiences/subscribers/) for the full reference. | Endpoint | Method | Path | Scope | | --- | --- | --- | --- | | [Add a subscriber](/docs/api-reference/audiences/subscribers/add/) | `POST` | `/audiences/{audience_id}/subscribers` | `full` | | [Retrieve a subscriber](/docs/api-reference/audiences/subscribers/get/) | `GET` | `/audiences/{audience_id}/subscribers/{id}` | `full` | | [Update a subscriber](/docs/api-reference/audiences/subscribers/update/) | `POST` | `/audiences/{audience_id}/subscribers/{id}` | `full` | | [List subscribers](/docs/api-reference/audiences/subscribers/list/) | `GET` | `/audiences/{audience_id}/subscribers` | `full` | | [Delete a subscriber](/docs/api-reference/audiences/subscribers/delete/) | `DELETE` | `/audiences/{audience_id}/subscribers/{id}` | `full` | ## Contacts Manage contact profiles and custom fields, in bulk or one at a time. See [Contacts](/docs/api-reference/contacts/) for the full reference. | Endpoint | Method | Path | Scope | | --- | --- | --- | --- | | [Create a contact](/docs/api-reference/contacts/create/) | `POST` | `/contacts` | `full` | | [Retrieve a contact](/docs/api-reference/contacts/get/) | `GET` | `/contacts/{id}` | `full` | | [Update a contact](/docs/api-reference/contacts/update/) | `POST` | `/contacts/{id}` | `full` | | [List contacts](/docs/api-reference/contacts/list/) | `GET` | `/contacts` | `full` | | [Bulk update contacts](/docs/api-reference/contacts/bulk/) | `POST` | `/contacts/bulk` | `full` | | [Export contacts](/docs/api-reference/contacts/export/) | `GET`, `POST` | `/contacts/export` | `full` | | [Delete a contact](/docs/api-reference/contacts/delete/) | `DELETE` | `/contacts/{id}` | `full` | ## Campaigns Create campaigns, choose their audiences, and send or schedule them. See [Campaigns](/docs/api-reference/campaigns/) for the full reference. | Endpoint | Method | Path | Scope | | --- | --- | --- | --- | | [Create a campaign](/docs/api-reference/campaigns/create/) | `POST` | `/campaigns` | `full` | | [Retrieve a campaign](/docs/api-reference/campaigns/get/) | `GET` | `/campaigns/{id}` | `full` | | [Update a campaign](/docs/api-reference/campaigns/update/) | `POST` | `/campaigns/{id}` | `full` | | [List campaigns](/docs/api-reference/campaigns/list/) | `GET` | `/campaigns` | `full` | | [Send or schedule a campaign](/docs/api-reference/campaigns/send/) | `POST` | `/campaigns/{id}/send` | `full` | | [Cancel a campaign](/docs/api-reference/campaigns/cancel/) | `POST` | `/campaigns/{id}/cancel` | `full` | | [Delete a campaign](/docs/api-reference/campaigns/delete/) | `DELETE` | `/campaigns/{id}` | `full` | ## Automations Build workflows from triggers and steps, run them, and inspect their runs. See [Automations](/docs/api-reference/automations/) for the full reference. | Endpoint | Method | Path | Scope | | --- | --- | --- | --- | | [Create an automation](/docs/api-reference/automations/create/) | `POST` | `/automations` | `full` | | [Retrieve an automation](/docs/api-reference/automations/get/) | `GET` | `/automations/{id}` | `full` | | [Update an automation](/docs/api-reference/automations/update/) | `POST` | `/automations/{id}` | `full` | | [List automations](/docs/api-reference/automations/list/) | `GET` | `/automations` | `full` | | [Delete an automation](/docs/api-reference/automations/delete/) | `DELETE` | `/automations/{id}` | `full` | | [Start an automation](/docs/api-reference/automations/start/) | `POST` | `/automations/{id}/start` | `full` | | [Pause an automation](/docs/api-reference/automations/pause/) | `POST` | `/automations/{id}/pause` | `full` | | [Stop an automation](/docs/api-reference/automations/stop/) | `POST` | `/automations/{id}/stop` | `full` | | [Trigger a run](/docs/api-reference/automations/trigger/) | `POST` | `/automations/{id}/trigger` | `full` | | [List runs](/docs/api-reference/automations/runs/) | `GET` | `/automations/{id}/runs` | `full` | | [Retrieve a run](/docs/api-reference/automations/run/) | `GET` | `/automations/{id}/runs/{run_id}` | `full` | | [Retrieve statistics](/docs/api-reference/automations/stats/) | `GET` | `/automations/{id}/stats` | `full` | | [Retrieve step statistics](/docs/api-reference/automations/step-stats/) | `GET` | `/automations/{id}/steps/{step_key}/stats` | `full` | ## Forms Create sign-up forms, publish them, and rotate their public token. See [Forms](/docs/api-reference/forms/) for the full reference. | Endpoint | Method | Path | Scope | | --- | --- | --- | --- | | [Create a form](/docs/api-reference/forms/create/) | `POST` | `/forms` | `full` | | [Retrieve a form](/docs/api-reference/forms/get/) | `GET` | `/forms/{id}` | `full` | | [Update a form](/docs/api-reference/forms/update/) | `POST` | `/forms/{id}` | `full` | | [List forms](/docs/api-reference/forms/list/) | `GET` | `/forms` | `full` | | [Publish a form](/docs/api-reference/forms/publish/) | `POST` | `/forms/{id}/publish` | `full` | | [Unpublish a form](/docs/api-reference/forms/unpublish/) | `POST` | `/forms/{id}/unpublish` | `full` | | [Reset the public token](/docs/api-reference/forms/reset-token/) | `POST` | `/forms/{id}/reset-token` | `full` | | [Delete a form](/docs/api-reference/forms/delete/) | `DELETE` | `/forms/{id}` | `full` | ## Templates Create template versions, publish one per alias, and use them when sending. See [Templates](/docs/api-reference/templates/) for the full reference. | Endpoint | Method | Path | Scope | | --- | --- | --- | --- | | [Create a template](/docs/api-reference/templates/create/) | `POST` | `/templates` | `full` | | [Retrieve a template](/docs/api-reference/templates/get/) | `GET` | `/templates/{id}` | `full` | | [Update a template](/docs/api-reference/templates/update/) | `POST` | `/templates/{id}` | `full` | | [List templates](/docs/api-reference/templates/list/) | `GET` | `/templates` | `full` | | [Publish a template](/docs/api-reference/templates/publish/) | `POST` | `/templates/{id}/publish` | `full` | | [Delete a template](/docs/api-reference/templates/delete/) | `DELETE` | `/templates/{id}` | `full` | ## Suppressions Read and manage the addresses Emailit will not send to. See [Suppressions](/docs/api-reference/suppressions/) for the full reference. | Endpoint | Method | Path | Scope | | --- | --- | --- | --- | | [Create a suppression](/docs/api-reference/suppressions/create/) | `POST` | `/suppressions` | `full` | | [Retrieve a suppression](/docs/api-reference/suppressions/get/) | `GET` | `/suppressions/{id}` | `full` | | [Update a suppression](/docs/api-reference/suppressions/update/) | `POST` | `/suppressions/{id}` | `full` | | [List suppressions](/docs/api-reference/suppressions/list/) | `GET` | `/suppressions` | `full` | | [Delete a suppression](/docs/api-reference/suppressions/delete/) | `DELETE` | `/suppressions/{id}` | `full` | ## Webhooks Register endpoints that receive signed event notifications. See [Webhooks](/docs/api-reference/webhooks/) for the full reference. | Endpoint | Method | Path | Scope | | --- | --- | --- | --- | | [Create a webhook](/docs/api-reference/webhooks/create/) | `POST` | `/webhooks` | `full` | | [Retrieve a webhook](/docs/api-reference/webhooks/get/) | `GET` | `/webhooks/{id}` | `full` | | [Update a webhook](/docs/api-reference/webhooks/update/) | `POST` | `/webhooks/{id}` | `full` | | [List webhooks](/docs/api-reference/webhooks/list/) | `GET` | `/webhooks` | `full` | | [Delete a webhook](/docs/api-reference/webhooks/delete/) | `DELETE` | `/webhooks/{id}` | `full` | | [Send a test event](/docs/api-reference/webhooks/test/) | `POST` | `/webhooks/{id}/test` | `full` | | [Rotate the signing secret](/docs/api-reference/webhooks/reset-secret/) | `POST` | `/webhooks/{id}/reset-secret` | `full` | | [Retry failed requests](/docs/api-reference/webhooks/retry-failed/) | `POST` | `/webhooks/{id}/retry-failed` | `full` | | [Retry one request](/docs/api-reference/webhooks/retry-request/) | `POST` | `/webhooks/{id}/requests/{request_id}/retry` | `full` | ## Events Read the event stream behind webhooks: deliveries, bounces, opens and more. See [Events](/docs/api-reference/events/) for the full reference. | Endpoint | Method | Path | Scope | | --- | --- | --- | --- | | [List events](/docs/api-reference/events/list/) | `GET` | `/events` | `full` | | [Retrieve an event](/docs/api-reference/events/get/) | `GET` | `/events/{id}` | `full` | ## Email verification Verify a single address in real time. See [Email verification](/docs/api-reference/email-verifications/) for the full reference. | Endpoint | Method | Path | Scope | | --- | --- | --- | --- | | [Verify an address](/docs/api-reference/email-verifications/verify/) | `POST` | `/email-verifications` | `full` | ## Verification lists Verify up to 10,000 addresses at once and export the results. See [Verification lists](/docs/api-reference/email-verifications/lists/) for the full reference. | Endpoint | Method | Path | Scope | | --- | --- | --- | --- | | [Create a list](/docs/api-reference/email-verifications/lists/create/) | `POST` | `/email-verification-lists` | `full` | | [List lists](/docs/api-reference/email-verifications/lists/list/) | `GET` | `/email-verification-lists` | `full` | | [Retrieve a list](/docs/api-reference/email-verifications/lists/get/) | `GET` | `/email-verification-lists/{id}` | `full` | | [List results](/docs/api-reference/email-verifications/lists/results/) | `GET` | `/email-verification-lists/{id}/results` | `full` | | [Export results](/docs/api-reference/email-verifications/lists/export/) | `GET` | `/email-verification-lists/{id}/export` | `full` | --- Source: https://emailit.com/docs/api-reference/endpoints/ --- # Errors > How the Emailit API reports errors. Response body formats, HTTP status codes and what they mean, and fixes for the error messages you'll see most. The Emailit API uses HTTP status codes to tell you whether a request worked. Codes in the `2xx` range mean success, `4xx` codes mean something about the request needs to change, and `5xx` codes mean something went wrong on our side. This page describes the error bodies, every status code the API returns and how to fix the most common errors. ## Error response formats Every error body is a JSON object with an `error` field. The exact shape depends on where the request failed. Write your error handling to read `error`, then `message` when it's present, then any extra fields the endpoint documents. ### Request errors Authentication failures, permission errors, malformed JSON and other errors raised before an endpoint runs use the standard HTTP error format: ```json { "statusCode": 401, "error": "Unauthorized", "message": "API key required" } ``` | Field | Description | | --- | --- | | `statusCode` | The HTTP status code. | | `error` | The HTTP reason phrase, such as `Unauthorized` or `Forbidden`. | | `message` | What went wrong, in plain language. | ### Validation errors When a query parameter or body field has the wrong type, is missing, or is out of range, the API rejects the request with `400` before it runs and lists each problem in `details`: ```json { "statusCode": 400, "error": "Bad Request", "message": "Validation error", "details": [ { "instancePath": "/limit", "schemaPath": "#/properties/limit/maximum", "keyword": "maximum", "params": { "comparison": "<=", "limit": 100 }, "message": "must be <= 100" } ] } ``` `instancePath` points to the field (`/limit`, `/to`, `/attachments/0/filename`), and `message` describes the rule it broke. On a few endpoints, such as [Send an email](/docs/api-reference/emails/send/), these errors return only `{"error": "Bad Request"}`. ### Resource errors Errors raised by an endpoint, such as a missing object or a duplicate name, return `error` and often `message`: ```json { "error": "Email not found", "message": "Email with ID 'em_4KYof1ZzXndZE2VPi0DgULiekG8' not found in your workspace" } ``` Some errors add fields that help you recover: | Field | Returned with | Contains | | --- | --- | --- | | `existing` | `409` when you create a duplicate domain, API key, audience, contact or subscriber | The object that already exists, so you can use it instead. | | `usage` | `422` when a plan limit is reached | `used`, `limit` and, for audiences, `plan`. | | `required_plan` | `403` with `error: "plan_required"` | The lowest plan that includes the feature, such as `pro`. | | `code` | Some `403` and `422` errors | A stable machine-readable code, such as `unverified_workspace_recipient` or `events_offset_too_large`. | | `missing` | `404` from [Bulk update contacts](/docs/api-reference/contacts/bulk/) | The contact IDs that weren't found. | ### Send validation errors [Send an email](/docs/api-reference/emails/send/) and [Forward an email](/docs/api-reference/emails/forward/) check the whole message at once and return every problem in `validation_errors`: ```json { "error": "Validation failed", "validation_errors": [ "Missing required field: subject", "Invalid to email address at index 1: ada@example" ] } ``` ### Field errors Templates, campaigns and automations return validation problems grouped by field: ```json { "message": "Validation failed", "errors": { "alias": ["Alias must contain only lowercase letters (a-z), numbers (0-9), underscores (_), and hyphens (-)"] } } ``` ### Rate limit errors `429` responses from the sending endpoints include the limit you hit and how long to wait. See [Rate limits](/docs/api-reference/rate-limits/). ```json { "error": "Rate limit exceeded", "message": "Too many requests. Maximum 2 messages per second allowed.", "limit": 2, "current": 2, "retry_after": 1 } ``` ## HTTP status codes | Code | Meaning | Typical causes in the Emailit API | | --- | --- | --- | | `200` | OK | The request worked. Sends, updates, deletes and reads return `200`. | | `201` | Created | A domain, API key, audience, subscriber, contact, template, webhook or other object was created. | | `202` | Accepted | A DMARC report upload was accepted for processing. | | `204` | No Content | A form was deleted. The response has no body. | | `400` | Bad Request | Invalid JSON, a missing required field, a value of the wrong type or out of range, an invalid `Idempotency-Key`, or no fields to update. | | `401` | Unauthorized | The API key is missing, invalid, deleted or regenerated, or an OAuth token expired. See [Authentication](/docs/api-reference/authentication/#authentication-errors). | | `402` | Payment Required | The workspace doesn't have enough credits for the send, retry or verification. | | `403` | Forbidden | The key's scope doesn't allow the endpoint, a domain-restricted key sent from another domain, the workspace is suspended or not yet verified, the sending domain is paused, or the feature needs a higher plan. | | `404` | Not Found | The object doesn't exist in this workspace, or a template alias has no published version. | | `409` | Conflict | An object with the same name or email already exists, or a request with the same `Idempotency-Key` is still running. | | `413` | Payload Too Large | The composed email is larger than 40 MB, or a DMARC report upload is larger than 10 MB. | | `422` | Unprocessable Entity | The request is valid but can't be done right now: the `from` domain isn't verified, an attachment couldn't be fetched, the email's status doesn't allow cancel or retry, its content was already purged, or a plan limit was reached. | | `429` | Too Many Requests | The workspace hit its per-second or daily sending limit, or the hourly forward limit. | | `500` | Internal Server Error | Something failed on our side. Retry with backoff, and contact support if it persists. | | `503` | Service Unavailable | A temporary outage of a dependency, such as the idempotency store or the authentication database. Retry with backoff. | ## Common errors and how to fix them | Status | `error` | Cause | Fix | | --- | --- | --- | --- | | `400` | `Validation failed` | A send is missing `from`, `to`, `subject` or content, or has an invalid address or attachment. | Fix each item listed in `validation_errors`. | | `400` | `Invalid JSON in request body` (in `message`) | The body isn't valid JSON. | Check quoting and trailing commas, and send `Content-Type: application/json`. | | `400` | `Invalid Idempotency-Key` | The key is longer than 256 characters or has characters other than letters, digits, `-` and `_`. | Use a UUID or a similar safe value. | | `402` | `Insufficient credits` | Credits ran out. Each recipient costs one credit. | Buy credits or turn on [auto-refill](/docs/billing/auto-refill/). | | `403` | `Workspace not verified` | The workspace is in sandbox mode and a recipient isn't a workspace member. | Request [production access](/docs/workspaces/production-access/). | | `403` | `Domain paused` | Sending from this domain is paused because of its [sending health](/docs/deliverability/sending-health/). | Fix the bounce or complaint problem, then contact support. | | `403` | `Domain not authorized` | The API key is restricted to another sending domain. | Send from the key's domain or use another key. | | `403` | `plan_required` | The feature, such as DMARC reports or webhook filters, isn't on your plan. | Upgrade to the plan in `required_plan`. | | `404` | `Template not found` | The template ID doesn't exist, or the alias has no published version. | [Publish](/docs/api-reference/templates/publish/) a version of the template. | | `409` | `… already exists` | You created an object with a name or email that's already taken. | Use the object in `existing`, or pick another name. | | `409` | `Idempotency key in progress` | Another request with the same key hasn't finished. | Wait a moment and retry with the same key. | | `413` | `Message too large` | The email, including attachments, is over 40 MB. | Send large files as links instead of attachments. | | `422` | `Domain not verified` | The `from` address isn't on a verified sending domain of this workspace. | [Verify the domain](/docs/domains/verification/) or change `from`. | | `422` | `Attachment error` | An attachment `url` couldn't be fetched within 30 seconds, isn't reachable, or is larger than 25 MB. | Check the URL is public and the file is small enough, or send `content` instead. | | `422` | `Cannot cancel email`, `Cannot retry email`, `Cannot update email` | The email's status doesn't allow the action, it's within 3 minutes of its scheduled time, or its content was purged. | Check the email's `status`. See each endpoint for the rules. | | `422` | `Page is too deep` | You paged past offset 2,500 of [List events](/docs/api-reference/events/list/). | Narrow the results with `type` or `created_at` filters. | | `429` | `Rate limit exceeded`, `Daily limit exceeded` | The workspace hit its sending limit. | Wait `retry_after` seconds. See [Rate limits](/docs/api-reference/rate-limits/). | ## Retry safely - Retry `429`, `500` and `503` responses after a delay. Use the `retry-after` header when it's present, and exponential backoff otherwise. [Rate limits](/docs/api-reference/rate-limits/#retry-with-backoff) has example code. - Don't retry other `4xx` errors unchanged. They fail the same way until you fix the request. - When you retry a send after a timeout or a `5xx` error, reuse the same [`Idempotency-Key`](/docs/api-reference/idempotency/) so the email isn't sent twice. ## Related - [Authentication](/docs/api-reference/authentication/): Credentials, scopes and every authentication error. - [Rate limits](/docs/api-reference/rate-limits/): Sending limits, headers and backoff. - [Idempotency](/docs/api-reference/idempotency/): Retry sends without sending twice. - [Request logs](/docs/logs/request-logs/): See the request and response of every failed API call. --- Source: https://emailit.com/docs/api-reference/errors/ --- # Filtering and sorting > Filter and sort API list endpoints with key.condition=value query parameters, match and order. Every condition and filterable field per resource. List endpoints accept the same flat filter language in the query string: one query parameter per filter, combined with `match`, and sorted with `order` and `direction`. This page covers the syntax, the conditions for each field type, and every field you can filter and sort on, resource by resource. ## Syntax A filter is a query parameter named `.` with the value to compare against: ```text GET /v2/emails?status.exact=bounced&created_at.after=2026-09-01 ``` | Parameter | Description | | --- | --- | | `.=` | One filter. Add as many as you need, including several on the same key. | | `match` | How filters combine. `all` (default) returns rows that match every filter. `or` returns rows that match at least one. | | `order` | The key to sort by. Must be one of the sort keys of the endpoint. | | `direction` | `asc` or `desc`. If you pass `order` without `direction`, results sort ascending. | Without `order`, lists return the newest objects first. Filters are combined with any dedicated parameters of the endpoint, such as `search` or `type`, using AND. `match` only controls how the `key.condition` filters combine with each other. Unknown keys, conditions that don't fit the key's type, and values that can't be parsed (an invalid date, a non-number, an enum value that doesn't exist, or an empty value) are ignored rather than rejected. If a filter seems to have no effect, check its spelling. ### Legacy sort parameters Some endpoints also accept `sort=` with `order=asc` or `order=desc`. On [List contacts](/docs/api-reference/contacts/list/), [List templates](/docs/api-reference/templates/list/), [List automations](/docs/api-reference/automations/list/) and [List suppressions](/docs/api-reference/suppressions/list/), `order` only accepts `asc` or `desc`, so sort those lists with `sort=&order=` instead of `order=`. The legacy form works on every list endpoint. ## Conditions Each key has a type, and each type accepts its own conditions. | Condition | String | Number | Date | Boolean | Enum | Matches when the field… | | --- | --- | --- | --- | --- | --- | --- | | `exact` | Yes | Yes | Yes | Yes | Yes | equals the value. Strings compare case-sensitively. Dates compare the calendar day. | | `not_exact` | Yes | Yes | | Yes | Yes | doesn't equal the value. | | `contains` | Yes | | | | | contains the value, ignoring case. | | `not_contains` | Yes | | | | | doesn't contain the value, ignoring case. Empty fields match. | | `starts_with` | Yes | | | | | starts with the value, ignoring case. | | `ends_with` | Yes | | | | | ends with the value, ignoring case. | | `gt`, `gte` | | Yes | | | | is greater than (or equal to) the value. | | `lt`, `lte` | | Yes | | | | is less than (or equal to) the value. | | `before` | | | Yes | | | is earlier than the value. | | `after` | | | Yes | | | is later than the value. | | `empty` | Yes | Yes | Yes | | | has no value. For strings, an empty string counts as empty. | | `not_empty` | Yes | Yes | Yes | | | has a value. | Value formats: - **Dates.** Any ISO 8601 date or date-time, such as `2026-09-01` or `2026-09-01T14:30:00Z`. `before` and `after` are exclusive. - **Booleans.** `true` or `1` mean true. Any other value means false. - **Enums.** One of the values listed for the key below. - **`empty` and `not_empty`.** The value is ignored, but the parameter needs one. Use `1`, for example `spam_score.empty=1`. ## Examples Bounced or failed emails since September 1: ```text GET /v2/emails?status.exact=bounced&status.exact=failed&match=or&date_from=2026-09-01 ``` Because `match=or` applies to every filter in the request, you can't mix AND and OR. To combine a date range with alternative statuses on [List emails](/docs/api-reference/emails/list/), use the dedicated `date_from` parameter for the date as shown, since dedicated parameters always apply with AND. Contacts at Acme whose `plan` custom field is `pro`, sorted by email: ```text GET /v2/contacts?email.ends_with=%40acme.com&custom_fields.plan.exact=pro&sort=email&order=asc ``` Domains with a failing DKIM record, oldest first: ```text GET /v2/domains?dkim_status.not_exact=ok&order=created_at&direction=asc ``` ### URL encoding Encode reserved characters in values: `+` as `%2B`, `&` as `%26`, `#` as `%23`, a space as `%20` and `@` as `%40`. An unencoded `+` in an address like `ada+news@example.com` is read as a space. With cURL, `-G` and `--data-urlencode` handle the encoding for you: **cURL** ```bash curl -G https://api.emailit.com/v2/emails \ -H "Authorization: Bearer $EMAILIT_API_KEY" \ --data-urlencode "to.exact=ada+news@example.com" \ --data-urlencode "subject.contains=order #1042" \ --data-urlencode "order=created_at" \ --data-urlencode "direction=desc" ``` **Node.js** ```javascript const params = new URLSearchParams({ 'to.exact': 'ada+news@example.com', 'subject.contains': 'order #1042', order: 'created_at', direction: 'desc', }); const response = await fetch(`https://api.emailit.com/v2/emails?${params}`, { headers: { Authorization: `Bearer ${process.env.EMAILIT_API_KEY}` }, }); const { data } = await response.json(); ``` **Python** ```python import os import requests response = requests.get( "https://api.emailit.com/v2/emails", headers={"Authorization": f"Bearer {os.environ['EMAILIT_API_KEY']}"}, params={ "to.exact": "ada+news@example.com", "subject.contains": "order #1042", "order": "created_at", "direction": "desc", }, ) data = response.json()["data"] ``` ## Filterable fields Unless a note says otherwise, every key in these tables is also a sort key. ### Emails [List emails](/docs/api-reference/emails/list/) (`GET /emails`). | Key | Type | Notes | | --- | --- | --- | | `to` | string | Recipient address. | | `from` | string | Sender as sent, including any display name. | | `subject` | string | | | `status` | enum | `accepted`, `scheduled`, `delivered`, `loaded`, `clicked`, `attempted`, `bounced`, `failed`, `rejected`, `suppressed`, `received`, `complained`, `canceled`, `held` | | `tag` | string | The email's tag. Sending through the API or SMTP doesn't set a tag at the moment. | | `spam_score` | number | | | `created_at` | date | Doesn't widen the default 14-day window. Use `date_from` for older emails. | | `updated_at` | date | | | `api_key_id` | string | The `key_…` ID of the API key that sent the email. | | `sending_domain_id` | string | The `dom_…` ID of the sending domain. | Also accepts `search`, `type` (`outbound` or `inbound`), `date_from` and `date_to`, plus the older `status`, `rcpt_to`, `mail_from`, `subject`, `api_key_id` and `sending_domain_id` parameters. ### Domains [List domains](/docs/api-reference/domains/list/) (`GET /domains`). | Key | Type | Notes | | --- | --- | --- | | `name` | string | | | `created_at` | date | | | `spf_status` | enum | `ok`, `missing`, `invalid` | | `dkim_status` | enum | `ok`, `missing`, `invalid` | | `return_path_status` | enum | `ok`, `missing`, `invalid` | Also accepts `search` (domain name contains). ### API keys [List API keys](/docs/api-reference/api-keys/list/) (`GET /api-keys`). | Key | Type | Notes | | --- | --- | --- | | `name` | string | | | `scope` | string | `full` or `sending`. | | `type` | string | Credential type. Keys created in the dashboard or API are `api`. | | `created_at` | date | | Also accepts `search` (name contains). ### Audiences and subscribers [List audiences](/docs/api-reference/audiences/list/) (`GET /audiences`) accepts `name` (string) and `created_at` (date), plus `search`. [List subscribers](/docs/api-reference/audiences/subscribers/list/) (`GET /audiences/{id}/subscribers`): | Key | Type | Notes | | --- | --- | --- | | `email` | string | The contact's email address. | | `first_name` | string | | | `last_name` | string | | | `subscribed` | boolean | | | `created_at` | date | When the contact joined the audience. | Also accepts `search` (email, first or last name contains) and `subscribed=true|false`. ### Contacts [List contacts](/docs/api-reference/contacts/list/) (`GET /contacts`) and [Export contacts](/docs/api-reference/contacts/export/). | Key | Type | Notes | | --- | --- | --- | | `email` | string | | | `first_name` | string | | | `last_name` | string | | | `name` | string | First and last name joined with a space. | | `audiences` | string | The alphabetically first audience name of the contact. | | `unsubscribed` | boolean | Filter only. | | `created_at` | date | | | `updated_at` | date | | | `audience_id` | string | Filter only. Only `exact` and `not_exact`. The value is an audience ID (`aud_…`). | | `custom_fields.` | string | Filter only. Replace `` with the custom field key, for example `custom_fields.company.contains=Acme`. Values compare as text. | Sort contacts with `sort` set to `email`, `first_name`, `last_name`, `name`, `audiences`, `created_at` or `updated_at`, and `order` set to `asc` or `desc`. Also accepts `search` (alias `q`), `audience_id` and `unsubscribed`. The older `filter[audience_id]`, `filter[unsubscribed]` and `filter[custom_fields][]` forms still work. ### Campaigns [List campaigns](/docs/api-reference/campaigns/list/) (`GET /campaigns`). | Key | Type | Notes | | --- | --- | --- | | `name` | string | | | `subject` | string | | | `status` | enum | `draft`, `scheduled`, `queued`, `sending`, `sent`, `archived` | | `created_at` | date | | | `sent_at` | date | | Also accepts `search` (name or subject contains). ### Forms [List forms](/docs/api-reference/forms/list/) (`GET /forms`). | Key | Type | Notes | | --- | --- | --- | | `name` | string | | | `type` | enum | `popup`, `full_page`, `flyout`, `embed`, `banner` | | `status` | enum | `draft`, `live` | | `created_at` | date | | Also accepts `search` (name contains). ### Automations [List automations](/docs/api-reference/automations/list/) (`GET /automations`). | Key | Type | Notes | | --- | --- | --- | | `name` | string | | | `status` | enum | Filter only. `draft`, `running`, `paused`, `stopped`, `archived` | | `context` | string | Filter only. `contact`, `email` or `event`. | | `created_at` | date | | Sort with `sort` set to `name`, `created_at`, `updated_at` or `last_triggered_at`, and `order` set to `asc` or `desc`. Also accepts `filter[name]`, `filter[status]` and `filter[context]`. [List runs](/docs/api-reference/automations/runs/) (`GET /automations/{id}/runs`) accepts `status` (string), `event` (string) and `created_at` (date). ### Templates [List templates](/docs/api-reference/templates/list/) (`GET /templates`). Only published versions are listed. | Key | Type | Notes | | --- | --- | --- | | `name` | string | | | `alias` | string | | | `editor` | string | Filter only. `html`, `text`, `dragit` or `tiptap`. | | `subject` | string | Filter only. | | `created_at` | date | | Sort with `sort` set to `name`, `alias`, `created_at`, `updated_at` or `published_at`, and `order` set to `asc` or `desc`. Also accepts `filter[name]`, `filter[alias]` and `filter[editor]`. ### Suppressions [List suppressions](/docs/api-reference/suppressions/list/) (`GET /suppressions`). | Key | Type | Notes | | --- | --- | --- | | `email` | string | | | `reason` | string | | | `type` | string | | | `created_at` | date | | | `keep_until` | date | Filter only. | Sort with `sort` set to `email`, `reason`, `type` or `created_at`, and `order` set to `asc` or `desc`. Also accepts `search` (alias `q`, email or reason contains). Suppressions also accept a JSON filter builder in the `filters` parameter (alias `filter`). Pass a URL-encoded JSON object: ```json { "match": "any", "rules": [ { "field": "reason", "operator": "contains", "value": "bounce" }, { "field": "type", "operator": "in", "value": "recipient,campaign" } ] } ``` | Property | Description | | --- | --- | | `match` | `all` (default) or `any`. | | `rules` | Up to 25 rules. Rules with an unknown field or operator are ignored. | | `rules[].field` | `email`, `reason`, `type` or `created_at`. | | `rules[].operator` | `equals`, `not_equals`, `contains`, `not_contains`, `starts_with`, `ends_with`, `greater_than`, `less_than`, `in`, `not_in`, `is_set` or `is_not_set`. | | `rules[].value` | The value to compare. For `in` and `not_in`, a comma-separated list. Not needed for `is_set` and `is_not_set`. | ### Events [List events](/docs/api-reference/events/list/) (`GET /events`). | Key | Type | Notes | | --- | --- | --- | | `type` | string | The event type, such as `email.delivered`. | | `created_at` | date | Any `created_at` filter replaces the default two-day window. | Also accepts `type` as a comma-separated list of exact event types, and `include_data`. ### Webhooks [List webhooks](/docs/api-reference/webhooks/list/) (`GET /webhooks`). | Key | Type | Notes | | --- | --- | --- | | `name` | string | | | `url` | string | | | `enabled` | boolean | | | `created_at` | date | | Also accepts `search` (name or URL contains). ### Email verification lists [List lists](/docs/api-reference/email-verifications/lists/list/) (`GET /email-verification-lists`) accepts `name` (string), `status` (string) and `created_at` (date), plus `search` and `status`. [List results](/docs/api-reference/email-verifications/lists/results/) (`GET /email-verification-lists/{id}/results`): | Key | Type | Notes | | --- | --- | --- | | `email` | string | | | `status` | string | | | `result` | string | For example `safe`, `invalid` or `disposable`. | | `risk` | string | `low`, `medium` or `high`. | | `created_at` | date | | Also accepts `status` and `result`. ### DMARC reports [List aggregate reports](/docs/api-reference/dmarc/list/) and [List forensic reports](/docs/api-reference/dmarc/forensic/). | Key | Type | Notes | | --- | --- | --- | | `type` | string | `aggregate` or `forensic`. | | `status` | string | | | `org_name` | string | The organization that sent the report. | | `created_at` | date | When Emailit received the report. | Also accepts `type`, `status`, `org_name`, `from` and `to`. DMARC lists page with `limit` and `offset`. See [Pagination](/docs/api-reference/pagination/). ## Related - [Pagination](/docs/api-reference/pagination/): Page through list results. - [All endpoints](/docs/api-reference/endpoints/): Every endpoint and the scope it needs. --- Source: https://emailit.com/docs/api-reference/filtering/ --- # Idempotency > Retry email sends safely with the Idempotency-Key header. Which endpoints support it, key format and scope, the 24-hour replay window and its errors. Network errors and timeouts leave you unsure whether a send went through. An idempotency key makes the send safe to retry: if Emailit already accepted a request with the same key, it returns the original response instead of sending the email again. This page is the reference for the `Idempotency-Key` header. For a walkthrough, see [Idempotent sends](/docs/email-api/idempotency/). ## Supported endpoints | Endpoint | | | --- | --- | | `POST /emails` | [Send an email](/docs/api-reference/emails/send/) | | `POST /emails/{id}/forward` | [Forward an email](/docs/api-reference/emails/forward/) | Other endpoints ignore the header. Creating a domain, API key, audience or contact is already safe to retry: a second request with the same name or email returns `409` and the `existing` object instead of creating a duplicate. ## Send a key Add an `Idempotency-Key` header with a value that's unique to the email you're sending, such as a UUID or an ID from your own system: **cURL** ```bash curl https://api.emailit.com/v2/emails \ -H "Authorization: Bearer $EMAILIT_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: order-1042-confirmation" \ -d '{ "from": "Acme ", "to": "ada@example.com", "subject": "Your order #1042", "html": "

Thanks for your order.

" }' ``` **Node.js** ```javascript const response = await fetch('https://api.emailit.com/v2/emails', { method: 'POST', headers: { Authorization: `Bearer ${process.env.EMAILIT_API_KEY}`, 'Content-Type': 'application/json', 'Idempotency-Key': `order-${order.id}-confirmation`, }, body: JSON.stringify({ from: 'Acme ', to: order.email, subject: `Your order #${order.id}`, html: '

Thanks for your order.

', }), }); ``` **Python** ```python import os import requests response = requests.post( "https://api.emailit.com/v2/emails", headers={ "Authorization": f"Bearer {os.environ['EMAILIT_API_KEY']}", "Idempotency-Key": f"order-{order['id']}-confirmation", }, json={ "from": "Acme ", "to": order["email"], "subject": f"Your order #{order['id']}", "html": "

Thanks for your order.

", }, timeout=30, ) ``` Generate the key once per email, before the first attempt, and send the same key on every retry of that email. ## How keys work | Rule | Details | | --- | --- | | Format | 1 to 256 characters. Letters, digits, hyphens (`-`) and underscores (`_`) only. | | Scope | Per workspace. Keys from different workspaces never collide, but both endpoints share one namespace, so don't reuse a send key for a forward. | | Lifetime | The response to a successful request is kept for 24 hours after it completes. | | Replays | A request with a stored key returns the stored response with status `200`. No email is sent, no credits are used, and the sending limits aren't counted. | | Matching | Only the key is compared, not the request body. A different request with a used key returns the first response. | | Failures | If a request fails with any error, nothing is stored and the key is released, so you can fix the request and retry with the same key. | | Concurrency | While a request with a key is still running, another request with the same key returns `409`. The lock is released when the first request finishes, and it expires after 15 minutes at most. | A replayed request still goes through authentication and the per-second and daily [sending limits](/docs/api-reference/rate-limits/) check, so it can return `401` or `429`. Forwards also count against the hourly forward limit, even when the response is replayed. ## Errors **400** ```json { "error": "Invalid Idempotency-Key" } ``` **409** ```json { "error": "Idempotency key in progress", "message": "A request with this Idempotency-Key is already being processed. Retry shortly with the same key." } ``` **503** ```json { "error": "Idempotency unavailable", "message": "Unable to process Idempotency-Key right now. Retry the request with the same key." } ``` | Status | `error` | Cause | What to do | | --- | --- | --- | --- | | `400` | `Invalid Idempotency-Key` | The key is empty, longer than 256 characters, or has other characters than letters, digits, `-` and `_`. | Use a UUID or another safe value. | | `409` | `Idempotency key in progress` | A request with the same key is still being processed. | Wait a second and retry with the same key. You'll get the stored response once the first request finishes. | | `503` | `Idempotency unavailable` | Emailit can't check the key right now. The request wasn't processed. | Retry with the same key after a short delay. | Emailit never sends a request without its idempotency check: if the key can't be checked, the request fails with `503` rather than risk a duplicate. ## Choose good keys - Derive the key from the thing you're notifying about, such as `order-1042-confirmation` or `password-reset-`, so a retry from another worker or after a restart uses the same key. - Use a fresh UUID only when the email has no natural ID, and store it with the job before the first attempt. - Don't reuse a key for a different email within 24 hours. You'd get the first email's response and the second email wouldn't be sent. ## Related - [Idempotent sends](/docs/email-api/idempotency/): A guide to retry-safe sending. - [Rate limits](/docs/api-reference/rate-limits/): Back off and retry with example code. --- Source: https://emailit.com/docs/api-reference/idempotency/ --- # API reference > The Emailit REST API at a glance. Base URL, authentication, JSON requests and responses, object IDs, versioning and every resource you can manage. The Emailit API is a REST API served over HTTPS. You send JSON, you get JSON back, and you authenticate every request with a Bearer token. Use it to send email and to manage everything else in a workspace: sending domains, API keys, contacts, audiences, campaigns, templates, webhooks and more. ## Base URL Every request goes to the version 2 base URL: ```text https://api.emailit.com/v2 ``` Paths in this reference are relative to it. For example, `POST /emails` means `POST https://api.emailit.com/v2/emails`. ## Make your first request This request sends one email. Replace the sender with an address on a [verified sending domain](/docs/domains/verification/) and set `EMAILIT_API_KEY` to one of your [API keys](/docs/developers/api-keys/). **cURL** ```bash curl https://api.emailit.com/v2/emails \ -H "Authorization: Bearer $EMAILIT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "from": "Acme ", "to": "ada@example.com", "subject": "Welcome to Acme", "html": "

Thanks for signing up.

" }' ``` **Node.js** ```javascript import { Emailit } from '@emailit/node'; const emailit = new Emailit(process.env.EMAILIT_API_KEY); const email = await emailit.emails.send({ from: 'Acme ', to: 'ada@example.com', subject: 'Welcome to Acme', html: '

Thanks for signing up.

', }); ``` **Python** ```python import os from emailit import EmailitClient client = EmailitClient(os.environ["EMAILIT_API_KEY"]) email = client.emails.send({ "from": "Acme ", "to": "ada@example.com", "subject": "Welcome to Acme", "html": "

Thanks for signing up.

", }) ``` The response is the new email object with its ID (`em_…`) and status `accepted`. See [Send an email](/docs/api-reference/emails/send/) for every option. ## Authentication Pass an API key or an OAuth access token in the `Authorization` header: ```http Authorization: Bearer secret_•••••••••••••••••••••••••••••••• ``` API keys start with `secret_` and belong to one workspace. A key has the `full` scope (every endpoint) or the `sending` scope (send endpoints only), and a sending key can be restricted to one sending domain. Requests without a valid key fail with `401`. See [Authentication](/docs/api-reference/authentication/). ## Requests and responses - **JSON in, JSON out.** Send request bodies as JSON with `Content-Type: application/json`. A body that isn't valid JSON returns `400` with the message `Invalid JSON in request body`. The maximum request body is 50 MB. - **Methods.** `GET` reads, `POST` creates and updates, and `DELETE` deletes. The API doesn't use `PUT` or `PATCH`. - **Objects.** Every object has an `object` field that names its type (`email`, `domain`, `api_key`, `audience`, `subscriber`, `contact`, …) and an `id`. - **Timestamps.** Dates are ISO 8601 strings in UTC with microsecond precision, for example `2026-10-01T09:30:12.482913Z`. Fields that aren't set are `null`. - **Lists.** List endpoints are paginated and most accept filters and sorting. See [Pagination](/docs/api-reference/pagination/) and [Filtering](/docs/api-reference/filtering/). - **Errors.** Failed requests return a `4xx` or `5xx` status code and a JSON body that explains the problem. See [Errors](/docs/api-reference/errors/). ## Object IDs IDs are strings made of a type prefix and 27 letters and digits, for example `em_4KYof1ZzXndZE2VPi0DgULiekG8`. IDs are case-sensitive and roughly ordered by creation time. | Prefix | Object | Prefix | Object | | --- | --- | --- | --- | | `em_` | Email | `aud_` | Audience | | `dom_` | Sending domain | `sub_` | Subscriber | | `key_` | API key | `con_` | Contact | | `tem_` | Template | `cmp_` | Campaign | | `sup_` | Suppression | `frm_` | Form | | `wh_` | Webhook | `fsub_` | Form submission | | `whr_` | Webhook request | `aut_` | Automation | | `evt_` | Event | `aur_` | Automation run | | `dmr_` | DMARC report | `ev_` | Email verification | | `evl_` | Verification list | | | Some resources also accept a readable identifier in the path. Domains, API keys, audiences, campaigns and webhooks accept their name (`GET /domains/acme.com`). Contacts and suppressions accept an email address, and subscribers accept the contact's email address. URL-encode names and addresses that contain special characters. Domains created before the switch to `dom_` IDs keep their `sd_` or `sed_` ID, and those IDs still work. ## Versioning The current version is `v2`, and it's part of the base URL. New fields and endpoints are added to `v2` without a version change, so write clients that ignore fields they don't recognize. See [Versioning](/docs/api-reference/versioning/). ## Resources - [Emails](/docs/api-reference/emails/): Send email, read messages and their content, and schedule, cancel, retry or forward them. - [Domains](/docs/api-reference/domains/): Add sending domains, read their DNS records and verify them. - [DMARC reports](/docs/api-reference/dmarc/): Read aggregate and forensic DMARC reports for a domain, or upload your own. - [API keys](/docs/api-reference/api-keys/): Create, rename, regenerate and delete the API keys of a workspace. - [Audiences](/docs/api-reference/audiences/): Manage the subscriber lists used by campaigns and sign-up forms. - [Subscribers](/docs/api-reference/audiences/subscribers/): Add, update and remove the subscribers of an audience. - [Contacts](/docs/api-reference/contacts/): Manage contact profiles and custom fields, one at a time or in bulk. - [Campaigns](/docs/api-reference/campaigns/): Create campaigns, choose their audiences, and send or schedule them. - [Automations](/docs/api-reference/automations/): Build workflows from triggers and steps, run them and inspect their runs. - [Forms](/docs/api-reference/forms/): Create sign-up forms, publish them and rotate their public token. - [Templates](/docs/api-reference/templates/): Create template versions, publish one per alias and send with it. - [Suppressions](/docs/api-reference/suppressions/): Read and manage the addresses Emailit won't send to. - [Webhooks](/docs/api-reference/webhooks/): Register endpoints that receive signed event notifications. - [Events](/docs/api-reference/events/): Read the event stream behind webhooks: deliveries, bounces, opens and more. - [Email verification](/docs/api-reference/email-verifications/): Verify a single address in real time. - [Verification lists](/docs/api-reference/email-verifications/lists/): Verify up to 10,000 addresses at once and export the results. For a single table of every endpoint and the scope it needs, see [All endpoints](/docs/api-reference/endpoints/). ## SDKs Official libraries wrap the API for the most common languages. They're open source on [GitHub](https://github.com/emailit). | Language | Package | Guide | | --- | --- | --- | | Node.js | `@emailit/node` | [Node.js](/docs/frameworks/nodejs/) | | Python | `emailit` | [Python](/docs/frameworks/python/) | | PHP | `emailit/emailit-php` | [PHP](/docs/frameworks/php/) | | Laravel | `emailit/emailit-laravel` | [Laravel](/docs/frameworks/laravel/) | | Ruby | `emailit` | [Ruby on Rails](/docs/frameworks/rails/) | | Go | `github.com/emailit/emailit-go/v2` | [Go](/docs/frameworks/go/) | | Java | `com.emailit` | [Java](/docs/frameworks/java/) | | .NET | `Emailit` | [.NET](/docs/frameworks/dotnet/) | | Rust | `emailit` | [SDKs](/docs/sdks/) | ## Webhooks and events Instead of polling for status changes, register a [webhook](/docs/webhooks/) and Emailit posts signed batches of events to your endpoint as they happen: deliveries, bounces, opens, clicks, new contacts and more. The same events are available from [List events](/docs/api-reference/events/list/). See [Event types](/docs/webhooks/event-types/) for the full list. ## MCP server The hosted MCP server at `https://api.emailit.com/mcp` lets AI assistants such as ChatGPT, Claude, Cursor, Codex and Grok call this API on your behalf: 109 tools cover every resource on this page. Assistants sign in with OAuth or use an API key, with the same scopes. See [MCP server](/docs/mcp/) and the [tool reference](/docs/mcp/tools/). ## Related - [Authentication](/docs/api-reference/authentication/): API keys, scopes, domain restrictions and OAuth tokens. - [Rate limits](/docs/api-reference/rate-limits/): Sending limits, response headers and how to back off. - [Errors](/docs/api-reference/errors/): Error formats, status codes and common fixes. - [Send your first email](/docs/quickstart/api/): A step-by-step quickstart from API key to inbox. --- Source: https://emailit.com/docs/api-reference/ --- # Pagination > Page through Emailit API list endpoints with page and limit, read next_page_url, handle the per_page and offset formats and the email and event time windows. List endpoints return results one page at a time. Most of them use page numbers with `page` and `limit`, templates and automations use `page` and `per_page`, and DMARC endpoints use `limit` and `offset`. This page explains each format, the time windows on the email and event lists, and how to loop through every page. ## Page and limit Most list endpoints accept two query parameters: | Parameter | Description | | --- | --- | | `page` | The page to return, starting at `1`. Default `1`. | | `limit` | Objects per page, from `1` to `100`. Values outside that range return a `400` validation error. | The default page size depends on the endpoint: | Default `limit` | Endpoints | | --- | --- | | 10 | Domains, API keys, audiences, contacts, campaigns, forms, suppressions, webhooks, email verification lists | | 25 | Emails, subscribers | | 50 | Email verification list results | | 100 | Events | [List subscribers](/docs/api-reference/audiences/subscribers/list/) also accepts `per_page` as an alias for `limit`. ### Response ```json { "data": [ { "object": "domain", "id": "dom_4K468YrjOkR1wwdhqiO0G9XEUey", "name": "acme.com" } ], "next_page_url": "/v2/domains?page=3&limit=10", "previous_page_url": "/v2/domains?page=1&limit=10" } ``` | Field | Description | | --- | --- | | `data` | The objects on this page, newest first unless you [sort](/docs/api-reference/filtering/) them. | | `next_page_url` | Path of the next page, or `null` on the last page. | | `previous_page_url` | Path of the previous page, or `null` on the first page. | The page URLs are paths, not full URLs, and on most endpoints they only carry `page` and `limit`, not your filters. To fetch the next page, repeat your own request with `page` increased by one, and stop when `next_page_url` is `null`. Some lists add fields next to `data`: | Field | Endpoints | Contains | | --- | --- | --- | | `total_records` | Contacts, audiences, campaigns, forms | The number of objects that match, across all pages. | | `usage` | Webhooks | `used` and `limit` webhook endpoints for your plan, and `filters_allowed`. | | `usage` | Subscribers | `used` and `limit` subscribers for the audience, and the `plan`. | | `domain_limit`, `domain_count`, `plan_name` | Domains | How many sending domains your plan allows (`null` means no limit), how many you have, and the plan name. | ## Page and per_page [List templates](/docs/api-reference/templates/list/), [List automations](/docs/api-reference/automations/list/) and [List runs](/docs/api-reference/automations/runs/) use `per_page` instead of `limit` and return page counts instead of page URLs: | Parameter | Description | | --- | --- | | `page` | The page to return, starting at `1`. Default `1`. | | `per_page` | Objects per page, from `1` to `100`. Default `25`. | ```json { "data": [], "total_records": 42, "per_page": 25, "current_page": 2, "total_pages": 2 } ``` Keep requesting pages while `current_page` is less than `total_pages`. ## Limit and offset The DMARC endpoints skip a number of rows instead of counting pages: | Parameter | Description | | --- | --- | | `limit` | Rows to return. Reports: default `25`, maximum `100`. Sources, countries, networks and reporters: default `50`, maximum `200`. | | `offset` | Rows to skip. Default `0`. | [List aggregate reports](/docs/api-reference/dmarc/list/) and [List forensic reports](/docs/api-reference/dmarc/forensic/) return the total count, so you know when to stop: ```json { "data": [], "meta": { "total": 130, "limit": 25, "offset": 50 } } ``` The breakdown endpoints return `meta` with `limit` and `offset` only. Stop when a page has fewer rows than `limit`. ## Time windows Two lists only look at recent data unless you ask for more: - **Emails.** [List emails](/docs/api-reference/emails/list/) returns emails from the last 14 days by default. Pass `date_from` (and optionally `date_to`) as a date such as `2026-08-01` to search further back. `created_at` filters don't widen the window, so use `date_from`. - **Events.** [List events](/docs/api-reference/events/list/) returns events from the last two days by default. Any `created_at` filter, such as `created_at.after=2026-09-01`, replaces that window. The list also stops at an offset of 2,500: a page that starts beyond the 2,500th event returns `422` with the code `events_offset_too_large`. Narrow the results with `type` or `created_at` filters to reach older events. ## Fetch every page Loop until the API says there's no next page. These examples collect every contact in an audience: **cURL** ```bash page=1 while : ; do response=$(curl -s -G https://api.emailit.com/v2/contacts \ -H "Authorization: Bearer $EMAILIT_API_KEY" \ --data-urlencode "audience_id=aud_4KbFmQPy1feMCGAY8F8pAhiZ1u2" \ --data-urlencode "limit=100" \ --data-urlencode "page=$page") echo "$response" | jq -c '.data[]' [ "$(echo "$response" | jq -r '.next_page_url')" = "null" ] && break page=$((page + 1)) done ``` **Node.js** ```javascript async function* listAll(path, params = {}) { for (let page = 1; ; page++) { const query = new URLSearchParams({ ...params, limit: '100', page: String(page) }); const response = await fetch(`https://api.emailit.com/v2${path}?${query}`, { headers: { Authorization: `Bearer ${process.env.EMAILIT_API_KEY}` }, }); if (!response.ok) throw new Error(`Emailit ${response.status}`); const body = await response.json(); yield* body.data; if (!body.next_page_url) return; } } for await (const contact of listAll('/contacts', { audience_id: 'aud_4KbFmQPy1feMCGAY8F8pAhiZ1u2' })) { console.log(contact.email); } ``` **Python** ```python import os import requests def list_all(path, params=None): session = requests.Session() session.headers["Authorization"] = f"Bearer {os.environ['EMAILIT_API_KEY']}" page = 1 while True: response = session.get( f"https://api.emailit.com/v2{path}", params={**(params or {}), "limit": 100, "page": page}, timeout=30, ) response.raise_for_status() body = response.json() yield from body["data"] if not body["next_page_url"]: return page += 1 for contact in list_all("/contacts", {"audience_id": "aud_4KbFmQPy1feMCGAY8F8pAhiZ1u2"}): print(contact["email"]) ``` ## Consistent results Pages are computed when you request them. If objects are created or deleted while you page through a newest-first list, objects can move between pages, so you might see one twice or miss one. For a stable export of a list that keeps growing, sort oldest first with `order=created_at&direction=asc` (or `sort=created_at&order=asc` on contacts and suppressions), and skip IDs you've already seen. ## Related - [Filtering and sorting](/docs/api-reference/filtering/): Narrow and order list results. - [Rate limits](/docs/api-reference/rate-limits/): Keep bulk reads reasonable. --- Source: https://emailit.com/docs/api-reference/pagination/ --- # Rate limits > How Emailit limits sending per workspace, the ratelimit headers on every send, what a 429 looks like, other endpoint limits and how to retry with backoff. Emailit limits how fast and how much a workspace can send, not how many API calls you make. This page explains the sending limits, the headers that report them, the few endpoints with their own limits, and how to back off when you get a `429`. ## Sending limits Every workspace has two sending limits. New workspaces start with these defaults on every plan: | Limit | Default | Window | | --- | --- | --- | | Per second | 2 emails | Sliding one-second window | | Per day | 5,000 emails | Calendar day in UTC, resets at 00:00 UTC | How the limits apply: - **Per workspace.** All API keys and the [SMTP relay](/docs/smtp/) share the same counters. Sending through SMTP uses up the same allowance as the API. - **Counted by recipients.** Each unique address across `to`, `cc` and `bcc` is one email. A request with three recipients counts as three. - **Only sends count.** The limits apply to [Send an email](/docs/api-reference/emails/send/) and [Forward an email](/docs/api-reference/emails/forward/). Reading data and managing resources don't count against them. - **Checked before, counted after.** A send is accepted as long as the workspace hasn't reached the limit yet, and its recipients are added to the counters afterward. One request with many recipients can take you past the limit, and the next requests get `429` until the window clears. You can see the current limits and today's usage in the **Sending Limits** card on the **Dashboard** home page. ## Rate limit headers Responses from the send and forward endpoints include these headers: | Header | Description | | --- | --- | | `ratelimit-limit` | Emails the workspace can send per second. | | `ratelimit-remaining` | Emails left in the current one-second window. | | `ratelimit-reset` | Seconds until the per-second window resets. | | `ratelimit-daily-limit` | Emails the workspace can send per day. | | `ratelimit-daily-remaining` | Emails left today. | | `ratelimit-daily-reset` | Seconds until the daily limit resets at 00:00 UTC. | | `retry-after` | Seconds to wait before retrying. Only sent with `429` responses. | ```http HTTP/1.1 200 OK content-type: application/json; charset=utf-8 ratelimit-limit: 2 ratelimit-remaining: 1 ratelimit-reset: 1 ratelimit-daily-limit: 5000 ratelimit-daily-remaining: 4812 ratelimit-daily-reset: 52113 ``` ## When you hit a limit A send over the limit returns `429 Too Many Requests` with a `retry-after` header. The body says which limit you hit: **429 Per second** ```json { "error": "Rate limit exceeded", "message": "Too many requests. Maximum 2 messages per second allowed.", "limit": 2, "current": 2, "retry_after": 1 } ``` **429 Daily** ```json { "error": "Daily limit exceeded", "message": "Daily sending limit of 5000 messages has been reached.", "limit": 5000, "current": 5000, "retry_after": 41760 } ``` | Field | Description | | --- | --- | | `error` | `Rate limit exceeded` for the per-second limit, `Daily limit exceeded` for the daily limit. | | `limit` | The limit that was reached. | | `current` | How many emails were already counted in the window. | | `retry_after` | Seconds to wait. `1` for the per-second limit, and the seconds until 00:00 UTC for the daily limit. | Nothing is sent and no credits are used when a request is rejected. Retry per-second rejections after a short wait. For daily rejections, queue the email and send it after the reset, or ask for a higher limit. ## Other limits A few endpoints have limits of their own: | Endpoint | Limit | Counted per | | --- | --- | --- | | `POST /emails/{id}/forward` | 3 per hour | Workspace | | `POST /webhooks/{id}/test` | 5 per minute | IP address | | Hosted subscribe and unsubscribe links (`/subscribe/{token}`, `/unsubscribe/{token}`) | 30 per minute | IP address | | Public form endpoints: load a form (`GET /forms/{token}`) | 60 per minute | IP address | | Public form endpoints: submit a form (`POST /forms/{token}/submit`) | 30 per minute | IP address | Forwards also count against the sending limits above. Over the forward limit, the API returns `429` with a `retry-after` header: ```json { "error": "too_many_requests", "message": "Forwarding is limited to 3 emails per hour for this workspace. Try again later." } ``` These limits are fixed and don't change with your plan or sending limits. ## All other endpoints Endpoints that read data or manage resources have no fixed per-endpoint limit. Keep your usage reasonable: use a small pool of concurrent requests rather than thousands in parallel, cache data that rarely changes, and use [webhooks](/docs/webhooks/) instead of polling for email status. ## Raise your limits - **Pro and Business.** Limits rise automatically as you send, based on your [sending health](/docs/deliverability/sending-health/). Emailit raises them at most once every seven days, and only while your sending health is good and none of your domains is paused. - **Every plan.** Ask for an increase on the **Dashboard** home page: select **Request Increase** in the **Sending Limits** card, enter the per-second and per-day limits you need, where your list comes from and why. The support team usually reviews requests within 24 hours. See [Limits](/docs/limits/) for every plan limit. ## Retry with backoff Retry `429`, `409` (idempotency key in progress), `500` and `503` responses. Wait for `retry-after` seconds when the header is present, and use exponential backoff with jitter when it isn't. Send an [`Idempotency-Key`](/docs/api-reference/idempotency/) so a retried send is never delivered twice, and don't wait out a daily limit in a request loop. **cURL** ```bash # curl retries 429 and 5xx responses and honors retry-after. curl https://api.emailit.com/v2/emails \ --retry 5 --retry-max-time 60 \ -H "Authorization: Bearer $EMAILIT_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: order-1042-confirmation" \ -d '{ "from": "Acme ", "to": "ada@example.com", "subject": "Your order #1042", "text": "Thanks for your order." }' ``` **Node.js** ```javascript import { randomUUID } from 'node:crypto'; const RETRYABLE = new Set([409, 429, 500, 503]); const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); export async function sendEmail(payload, maxAttempts = 5) { const idempotencyKey = randomUUID(); for (let attempt = 1; attempt <= maxAttempts; attempt++) { const response = await fetch('https://api.emailit.com/v2/emails', { method: 'POST', headers: { Authorization: `Bearer ${process.env.EMAILIT_API_KEY}`, 'Content-Type': 'application/json', 'Idempotency-Key': idempotencyKey, }, body: JSON.stringify(payload), }); const body = await response.json(); if (response.ok) return body; const retryAfter = Number(response.headers.get('retry-after')) || 0; // Daily limit: retry_after is hours away, so give up and queue it. if (!RETRYABLE.has(response.status) || retryAfter > 60 || attempt === maxAttempts) { throw new Error(`Emailit ${response.status}: ${body.message ?? body.error}`); } const backoff = Math.min(1000 * 2 ** (attempt - 1), 30_000); await sleep(retryAfter ? retryAfter * 1000 : backoff + Math.random() * 250); } } ``` **Python** ```python import os import random import time import uuid import requests RETRYABLE = {409, 429, 500, 503} def send_email(payload, max_attempts=5): idempotency_key = str(uuid.uuid4()) for attempt in range(1, max_attempts + 1): response = requests.post( "https://api.emailit.com/v2/emails", headers={ "Authorization": f"Bearer {os.environ['EMAILIT_API_KEY']}", "Idempotency-Key": idempotency_key, }, json=payload, timeout=30, ) if response.ok: return response.json() retry_after = int(response.headers.get("retry-after", 0)) # Daily limit: retry_after is hours away, so give up and queue it. if response.status_code not in RETRYABLE or retry_after > 60 or attempt == max_attempts: response.raise_for_status() backoff = min(2 ** (attempt - 1), 30) + random.random() / 4 time.sleep(retry_after or backoff) ``` ## Tips for high volume - Send from a queue with a fixed number of workers, and size the pool to your per-second limit. - Spread large batch jobs over the day instead of starting them all at once, so a single job doesn't use the whole daily allowance. - Watch `ratelimit-daily-remaining` and slow down before it reaches zero. - For newsletters to your audiences, use [campaigns](/docs/campaigns/) instead of looping over the send endpoint. ## Related - [Limits](/docs/limits/): Sending limits and plan quotas in one place. - [Sending health](/docs/deliverability/sending-health/): The score that drives automatic limit increases. - [Idempotency](/docs/api-reference/idempotency/): Retry sends without sending twice. - [Errors](/docs/api-reference/errors/): Every error format and status code. --- Source: https://emailit.com/docs/api-reference/rate-limits/ --- # Versioning > The Emailit API is on version 2. How the version is set, what changes without a new version, the deprecated v1 API and the move to prefixed object IDs. The API version is part of the base URL, so every request states the version it was written for. This page explains which versions exist, what can change within a version, and how object IDs changed in 2026. ## Versions | Version | Base URL | Status | | --- | --- | --- | | v2 | `https://api.emailit.com/v2` | Current. Released in October 2025. Everything in this reference is v2. | | v1 | `https://api.emailit.com/v1` | Deprecated. The previous generation of the API. It gets no new features and is no longer documented. | If you still call v1, move your integration to v2. The [SDKs](/docs/sdks/) all use v2. ## Changes within v2 Emailit adds to v2 without changing the version in the URL. These changes can happen at any time: - New endpoints and resources. - New optional request parameters and headers. - New fields in response objects and webhook payloads. - New event types, statuses and other enum values. Write clients that tolerate them: ignore response fields you don't recognize, don't fail on an unknown status or event type, and don't depend on the order of fields in a JSON object. Changes to the API are announced in the [changelog](/docs/changelog/). ## Object IDs In January 2026, every object moved to prefixed IDs: a type prefix followed by 27 letters and digits, such as `em_4KYof1ZzXndZE2VPi0DgULiekG8` for an email or `dom_4K468YrjOkR1wwdhqiO0G9XEUey` for a domain. All endpoints take and return these IDs. See [Object IDs](/docs/api-reference/#object-ids) for every prefix. - Store IDs as strings, and compare them exactly. They're case-sensitive. - Sending domains that existed before the change keep IDs that start with `sd_` or `sed_`. They work everywhere a `dom_` ID does, so don't validate domain IDs by prefix. - API keys are secrets, not IDs. New keys start with `secret_`, and older keys without the prefix keep working. An API key's ID starts with `key_`. ## Related - [Changelog](/docs/changelog/): New features and changes, newest first. - [API reference](/docs/api-reference/): Base URL, authentication and resources. --- Source: https://emailit.com/docs/api-reference/versioning/ --- # Audiences > Audiences are the lists your campaigns send to. Create audiences, manage subscribers, check your plan's subscriber limit and use audiences through the API. An audience is a named list of contacts, such as "Newsletter" or "Product updates". You choose one or more audiences as the recipients of a [campaign](/docs/campaigns/), and you can add people to an audience by hand, from an import, through the API or with a hosted sign-up URL. ## How audiences work Each person on an audience is a **subscriber**: a link between one [contact](/docs/contacts/) and the audience, with its own **Subscribed** flag. The same contact can be on several audiences and be subscribed to some and unsubscribed from others. - **Campaigns** send to subscribers whose **Subscribed** flag is on, whose contact isn't globally unsubscribed and whose address isn't suppressed. A contact on several selected audiences gets one copy. - **Unsubscribing** turns the flag off but keeps the subscriber on the list, so you keep the history. See [Unsubscribes](/docs/audiences/unsubscribes/). - **Deleting a subscriber** removes the person from the list. The contact stays in your workspace. ## The Audiences list Go to **Email Marketing → Audiences** to see your audiences with their **Name**, **Subscribers** and **Created** date. The **Subscribers** column shows how many subscribers each audience has against your plan's limit, for example `1.2k/10k`. Use the row menu to **Manage subscribers**, **Rename** or **Delete** an audience. To create one, select **Create audience**, enter a **Name** and select **Create**. Names must be unique in the workspace. ## The audience page Open an audience to manage its subscribers. The header shows the subscriber count and the limit badge, with these actions: | Action | What it does | | --- | --- | | **Rename** | Changes the audience name. | | **Subscribe URL** | Shows the hosted sign-up URL for this audience and lets you reset it. See [Subscribe URL](/docs/audiences/subscribe-url/). | | **Add subscriber** | Adds a person by email address. Replaced by **Upgrade** when a Pay as you go audience is full. | | **Delete** (in the menu) | Deletes the audience and all of its subscribers. The contacts stay in your workspace. This can't be undone. | Below it, the subscribers table lists **Email**, **First name**, **Last name**, **Subscribed** and **Created**, with search, filters and a row menu to **Edit** or **Delete** each subscriber. See [Manage subscribers](/docs/audiences/subscribers/). ## Limits Each plan limits how many subscribers one audience can hold. The number of audiences isn't limited. | | Pay as you go | Pro | Business | Custom | | --- | --- | --- | --- | --- | | Subscribers per audience | 10,000 | 50,000 | 250,000 | Unlimited | The limit counts every subscriber on the audience, including people who unsubscribed. To make room, delete subscribers you no longer need, [upgrade your plan](/docs/billing/plans/) or request a higher limit with **Request Increase** on the **Sending Limits** card of the dashboard home page. When an audience is full, **Add subscriber** is disabled and every way of adding people, including the API, imports and the subscribe URL, is rejected with `422` and a message such as "Pro includes 50,000 subscribers per audience.". The API error includes the current `usage`: ```json { "error": "Pro includes 50,000 subscribers per audience.", "usage": { "used": 50000, "limit": 50000, "plan": "pro" } } ``` ## Use the API The [Audiences API](/docs/api-reference/audiences/) lets you [create](/docs/api-reference/audiences/create/), [retrieve](/docs/api-reference/audiences/get/), [rename](/docs/api-reference/audiences/update/), [list](/docs/api-reference/audiences/list/) and [delete](/docs/api-reference/audiences/delete/) audiences, and the [Subscribers API](/docs/api-reference/audiences/subscribers/) manages the people on them. You can use an audience's name instead of its `aud_` ID in the URL. The API needs an API key with **Full Access**. ```bash curl https://api.emailit.com/v2/audiences \ -X POST \ -H "Authorization: Bearer $EMAILIT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Newsletter" }' ``` Audience responses include `subscribers_count` and a `usage` object with `used`, `limit` and `plan`. Creating, retrieving and updating an audience also returns its `token`, the secret part of its [subscribe URL](/docs/audiences/subscribe-url/). A duplicate name returns `409`. ## Events Audience and subscriber changes produce events that you can receive with [webhooks](/docs/webhooks/) and that start [automations](/docs/automations/): | Event | When | | --- | --- | | [`audience.created`, `audience.updated`, `audience.deleted`](/docs/webhooks/events/audience/) | An audience is created, renamed or deleted. Deleting an audience doesn't send an event for each of its subscribers. | | [`subscriber.created`](/docs/webhooks/events/subscriber/) | Someone is added to an audience from the dashboard or the API. Starts **Added to audience** automations. | | [`subscriber.updated`](/docs/webhooks/events/subscriber/) | A subscriber's details or **Subscribed** flag change. | | [`subscriber.deleted`](/docs/webhooks/events/subscriber/) | A subscriber is removed. Starts **Removed from audience** automations. | Imports and the subscribe URL don't send subscriber events. ## Next steps - [Manage subscribers](/docs/audiences/subscribers/): Add, edit, unsubscribe and remove people. - [Subscribe URL](/docs/audiences/subscribe-url/): Collect sign-ups from your own site. - [Unsubscribes](/docs/audiences/unsubscribes/): How opt-outs work across audiences. - [Send a campaign](/docs/campaigns/create/): Email the subscribers of your audiences. --- Source: https://emailit.com/docs/audiences/ --- # Subscribe URL > Every audience has a hosted subscribe URL that adds people without an API key. Learn the request format, how to call it safely, its limits and how to reset it. Each audience has a subscribe URL: a public endpoint that adds a person to the audience without an API key. Use it to connect a sign-up form on your site, or a no-code tool that can send a JSON request, to an audience. ```text POST https://api.emailit.com/subscribe/{token} ``` The `token` is secret to the audience. Anyone who has the URL can add addresses to the audience, so treat it like a password. ## Find the URL 1. **Open the audience.** Go to **Email Marketing → Audiences** and select the audience. 2. **Copy the URL.** Select **Subscribe URL** and copy the URL from the dialog. With the API, [Retrieve an audience](/docs/api-reference/audiences/get/) returns the same value in `token`. ## Request format Send a `POST` request with a JSON body and `Content-Type: application/json`. No `Authorization` header is needed. - `email` (string, required): The address to subscribe. Stored in lowercase. - `first_name` (string): The person's first name. Overwrites the stored first name of an existing contact. - `last_name` (string): The person's last name. Overwrites the stored last name of an existing contact. - `custom_fields` (object): Values keyed by [custom field](/docs/contacts/custom-fields/) key. Replaces all custom field values of an existing contact, so only send it when you have the full set. The endpoint only reads JSON. Form-encoded bodies, which a plain HTML `
` sends, are rejected with `400` and "Invalid JSON in request body". ### Responses | Status | Body | When | | --- | --- | --- | | `200` | `{ "message": "Subscribed successfully" }` | The person was added, resubscribed or was already subscribed. | | `400` | `{ "error": "Missing required field: email" }` or `{ "error": "Invalid email format" }` | The email is missing or malformed, or the body isn't JSON. | | `404` | `{ "error": "Audience not found" }` | The token is wrong or was reset. | | `422` | `{ "error": "...", "usage": { ... } }` | The audience reached its [subscriber limit](/docs/audiences/#limits). | | `429` | | More than 30 requests in a minute from the same IP address. | ### What a sign-up does - **New address:** Emailit creates the contact and subscribes it to the audience. - **Existing contact, not on the audience:** Emailit updates the names and custom fields you sent and subscribes the contact. - **Existing subscriber who had unsubscribed:** Emailit subscribes them again. - **Existing subscriber who is subscribed:** nothing changes except the subscription date, and the response is still `200`. Sign-ups through the subscribe URL don't send `subscriber.*` or `contact.*` [webhook events](/docs/webhooks/event-types/) and don't start **Added to audience** automations. They also don't change a contact's marketing status: if the contact was globally unsubscribed, campaigns keep skipping it until you resubscribe it on the Contacts list. ## Connect a sign-up form The subscribe URL has no bot protection, and it only accepts JSON. The safest setup is to post your form to your own server, check it there, and call the subscribe URL from the server. That keeps the token out of your page source and lets you block spam before it reaches your audience. 1. **Add the form to your page.** Post it to an endpoint on your own site. The hidden `website` field is a honeypot: people don't see it, but bots often fill it in. ```html title="signup.html"

``` 2. **Handle the form on your server.** Drop honeypot hits, check a CAPTCHA if you use one, then forward the fields to the subscribe URL as JSON. Keep the token in an environment variable such as `EMAILIT_SUBSCRIBE_TOKEN`. See the [server examples](#server-examples) below. 3. **Test it.** Submit the form with your own address and check that you appear in the audience's subscribers table. ### Server examples **Node.js** ```javascript title="server.js" import express from 'express'; const app = express(); app.post('/newsletter', express.urlencoded({ extended: false }), async (req, res) => { // Bots fill in the honeypot. Pretend it worked and stop. if (req.body.website) return res.redirect(303, '/thanks'); const response = await fetch( `https://api.emailit.com/subscribe/${process.env.EMAILIT_SUBSCRIBE_TOKEN}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: req.body.email, first_name: req.body.first_name || undefined, }), }, ); if (!response.ok) { const { error } = await response.json().catch(() => ({})); return res.status(response.status).send(error || 'Could not subscribe.'); } res.redirect(303, '/thanks'); }); app.listen(3000); ``` **PHP** ```php title="newsletter.php" true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['Content-Type: application/json'], CURLOPT_POSTFIELDS => json_encode([ 'email' => $_POST['email'] ?? '', 'first_name' => $_POST['first_name'] ?? null, ]), ]); $body = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); curl_close($ch); if ($status !== 200) { http_response_code($status); echo json_decode($body, true)['error'] ?? 'Could not subscribe.'; exit; } header('Location: /thanks', true, 303); ``` **cURL** ```bash curl "https://api.emailit.com/subscribe/$EMAILIT_SUBSCRIBE_TOKEN" \ -X POST \ -H "Content-Type: application/json" \ -d '{ "email": "ada@example.com", "first_name": "Ada" }' ``` If your page submits with JavaScript instead of a full page load, call your own endpoint with `fetch` and keep the Emailit call on the server: ```javascript title="signup.js" document.querySelector('#signup').addEventListener('submit', async (event) => { event.preventDefault(); const form = new FormData(event.target); const response = await fetch('/newsletter', { method: 'POST', body: new URLSearchParams(form) }); event.target.querySelector('.status').textContent = response.ok ? 'Thanks, you are subscribed.' : 'Something went wrong. Please try again.'; }); ``` ### Rate limit and higher volumes The subscribe URL accepts 30 requests per minute from each IP address and returns `429` above that. When your server forwards every sign-up, they all come from your server's IP, so the 30 per minute apply to your whole site. If you expect more, call [Add a subscriber](/docs/api-reference/audiences/subscribers/add/) from your server with an API key instead. That endpoint also returns `409` for people who are already subscribed and starts **Added to audience** automations, for example to send a welcome email. ## Confirm sign-ups The subscribe URL adds people immediately. Emailit doesn't send a confirmation email or ask the person to confirm their address (double opt-in). If you want confirmed sign-ups, build the confirmation into your own flow: when someone submits the form, store the request on your server and [send them an email](/docs/email-api/send-email/) with a confirmation link that points back to your site. Call the subscribe URL only after they open that link. ## Reset the URL Reset the token if the URL leaked or you're getting spam sign-ups. The old URL stops working immediately and returns `404`. 1. **Open the dialog.** On the audience page, select **Subscribe URL**. 2. **Reset.** Select **Reset token** and confirm. Emailit generates a new URL. 3. **Update your integrations.** Replace the token everywhere you use it, such as the `EMAILIT_SUBSCRIBE_TOKEN` variable on your server. Resetting the token is only available in the dashboard. ## Related - [Manage subscribers](/docs/audiences/subscribers/): Add people with the API and handle resubscribes. - [Unsubscribes](/docs/audiences/unsubscribes/): Let people leave your audiences. --- Source: https://emailit.com/docs/audiences/subscribe-url/ --- # Manage subscribers > Add people to an audience, edit them, turn their Subscribed flag on or off, and remove them, in the dashboard or with the Subscribers API. A subscriber is one contact's membership in one audience. This page shows how to add subscribers, change them, unsubscribe or resubscribe them, and remove them, and what each change does to the contact behind them. ## Before you begin - Create the audience first. See [Audiences](/docs/audiences/). - Check how much room the audience has. The limit counts every subscriber, including unsubscribed ones. See [Limits](/docs/audiences/#limits). - For the API, use an API key with **Full Access**. ## Add a subscriber **Dashboard** 1. **Open the audience.** Go to **Email Marketing → Audiences** and select the audience. 2. **Add the person.** Select **Add subscriber**, enter the **Email**, and optionally **First name** and **Last name**. 3. **Save.** Select **Add**. If no contact exists for the address, Emailit creates one. You can also add existing contacts from **Email Marketing → Contacts**: open a contact and select **Add to audience**, or select several contacts and use **Actions > Add to audience**. To add many people at once, [import a file](/docs/contacts/import-export/). **API** Call [Add a subscriber](/docs/api-reference/audiences/subscribers/add/) with the audience's ID or name: ```bash curl https://api.emailit.com/v2/audiences/aud_5hJ2kL8mNp4Qr/subscribers \ -X POST \ -H "Authorization: Bearer $EMAILIT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "email": "ada@example.com", "first_name": "Ada", "last_name": "Lovelace", "custom_fields": { "company": "Acme" } }' ``` ```json { "object": "subscriber", "id": "sub_7Rt2vX9kLm3Qp", "audience_id": "aud_5hJ2kL8mNp4Qr", "contact_id": "con_2kq8Vt4xLm7Rz", "email": "ada@example.com", "first_name": "Ada", "last_name": "Lovelace", "custom_fields": { "company": "Acme" }, "subscribed": true, "subscribed_at": "2026-10-01T09:30:00Z", "unsubscribed_at": null, "created_at": "2026-10-01T09:30:00Z", "updated_at": "2026-10-01T09:30:00Z" } ``` Only `email` is required. If you pass `first_name`, `last_name` or `custom_fields` for an existing contact, they overwrite the contact's values, and `custom_fields` replaces the whole object. ### When the person is already on the audience Adding an address that's already on the audience depends on its current state: | Current state | Result | API response | | --- | --- | --- | | Not on the audience | A new subscriber is created with **Subscribed** on. | `201` | | On the audience, **Subscribed** on | Nothing changes. The dashboard shows "Contact is already subscribed to this audience". | `409` with the subscriber in `existing` | | On the audience, unsubscribed | The subscriber is resubscribed: **Subscribed** turns on, `subscribed_at` is set to now and `unsubscribed_at` is cleared. | `200` | | The audience is full | Nothing is added. | `422` with `usage` | A resubscribe through this endpoint sends `subscriber.updated` and `subscriber.resubscribed` events and starts **Added to audience** automations, the same as a new subscriber. > **Caution:** Adding someone back after they unsubscribed resubscribes them. Only do this when the person asked to join again, for example by signing up on your site. ## Edit a subscriber In the audience's subscribers table, open the row menu and select **Edit**. You can change **First name**, **Last name** and the **Subscribed** switch. The email address is read-only here. With the API, call [Update a subscriber](/docs/api-reference/audiences/subscribers/update/). Identify the subscriber by its `sub_` ID or by email address, and send any of `email`, `first_name`, `last_name`, `custom_fields` and `subscribed`: ```bash curl https://api.emailit.com/v2/audiences/Newsletter/subscribers/ada@example.com \ -X POST \ -H "Authorization: Bearer $EMAILIT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "first_name": "Augusta", "subscribed": false }' ``` Names, email and custom fields belong to the contact, not the subscriber. Changing them here updates the contact everywhere, including in its other audiences. Only `subscribed` is specific to this audience. ## Turn Subscribed on or off The **Subscribed** flag decides whether campaigns to this audience reach the person. Turning it off keeps the subscriber on the list, records `unsubscribed_at` and leaves the contact's other audiences alone. Turning it back on sets a new `subscribed_at` and clears `unsubscribed_at`. | Where | Unsubscribe | Resubscribe | | --- | --- | --- | | Audience page | **Edit**, then turn off **Subscribed** | **Edit**, then turn on **Subscribed** | | Contact page | **Unsubscribe** in the audience's row menu | **Resubscribe** in the row menu | | API | [Update a subscriber](/docs/api-reference/audiences/subscribers/update/) with `"subscribed": false` | Update with `"subscribed": true`, or [add](/docs/api-reference/audiences/subscribers/add/) the address again | These changes send a `subscriber.updated` event. They don't record an unsubscribe on any campaign, so they don't appear in a campaign's **Unsubscribes** tab. When recipients unsubscribe themselves with the link in a campaign, they leave every audience at once. See [Unsubscribes](/docs/audiences/unsubscribes/). ## Remove a subscriber Removing a subscriber deletes the membership. Select **Delete** in the row menu on the audience page, **Remove from audience** on the contact page, or call [Delete a subscriber](/docs/api-reference/audiences/subscribers/delete/). | | Unsubscribe | Remove | | --- | --- | --- | | Person stays on the list | Yes, with **Subscribed** off | No | | Counts toward the audience limit | Yes | No | | Contact is kept | Yes | Yes | | Event | `subscriber.updated` | `subscriber.deleted`, which starts **Removed from audience** automations | | Adding the address again | Resubscribes the existing subscriber | Creates a new subscriber | Prefer unsubscribing when someone opts out, so you keep a record of it. Remove subscribers you added by mistake or no longer want to keep. To delete the person entirely, delete the contact, which removes it from every audience. ## List and find subscribers On the audience page, search by email or name, and filter by Email, First name, Last name, Subscribed or Created. With the API, [List subscribers](/docs/api-reference/audiences/subscribers/list/) returns every subscriber, including unsubscribed ones. Add `subscribed=true` or `subscribed=false` to narrow the list, `search` to match email or names, and `page` and `limit` (up to 100) to page through: ```bash curl "https://api.emailit.com/v2/audiences/aud_5hJ2kL8mNp4Qr/subscribers?subscribed=true&limit=100&page=1" \ -H "Authorization: Bearer $EMAILIT_API_KEY" ``` To look up one person, [Retrieve a subscriber](/docs/api-reference/audiences/subscribers/get/) by `sub_` ID or email address. ## Related - [Subscribe URL](/docs/audiences/subscribe-url/): Let people add themselves from your site. - [Contacts](/docs/contacts/): The person behind every subscriber. --- Source: https://emailit.com/docs/audiences/subscribers/ --- # Unsubscribes > How people opt out of campaigns, what the unsubscribe link and headers do, where unsubscribes appear in reports, and how to manage opt-outs yourself. This page explains how recipients unsubscribe from your campaigns, what Emailit adds to every campaign email so they can, and how you can see and change opt-outs yourself. It also covers how unsubscribes relate to transactional email. ## Three ways to stop a campaign Emailit checks three things before it sends a campaign email. Each one is a different kind of opt-out: | Level | Stored on | Set by | Stops | | --- | --- | --- | --- | | **Audience subscription** | The subscriber (`subscribed: false`) | The recipient's unsubscribe link, or you | Campaigns to that audience. The unsubscribe link turns off every audience the contact is on. | | **Marketing status** | The contact (`unsubscribed: true`) | You, from the Contacts list or the API | Campaigns to every audience. | | **Suppression** | The suppression list | Bounces, complaints, automations, or you | Campaigns, and with type `recipient` also every other email. See [Suppressions](/docs/suppressions/). | ## Unsubscribe links in campaign emails ### Headers added for you Every campaign email includes these headers, with a link unique to the recipient and the campaign: ```text List-Unsubscribe: List-Unsubscribe-Post: List-Unsubscribe=One-Click ``` `List-Unsubscribe-Post` marks the link as one-click ([RFC 8058](https://www.rfc-editor.org/rfc/rfc8058)). Mailbox providers such as Gmail and Yahoo use these headers to show their own **Unsubscribe** button next to the sender name, and they expect them from bulk senders. You don't need to add or configure anything. ### The link in your content Put a visible unsubscribe link in every campaign as well, with the `{{unsubscribe_url}}` merge tag: ```html

You're receiving this because you subscribed to Acme news. Unsubscribe

``` - In the **Dragit** editor, insert it as a link: `{{unsubscribe_url}}` is listed with the special links. - In the **Rich-text** editor, type `@` and pick **Unsubscribe URL**. - In the **HTML** editor, type the tag yourself, exactly as `{{unsubscribe_url}}`, with no spaces inside the braces. The **Send** step of the campaign wizard checks for the tag and shows "Missing unsubscribe link!" when it's not in the HTML. Emailit doesn't block the send, but anti-spam laws such as CAN-SPAM and GDPR, and the bulk sender rules of Gmail and Yahoo, expect a working unsubscribe link in marketing email. In [test sends](/docs/campaigns/test-and-schedule/#send-a-test), the link points to a test address and doesn't unsubscribe anyone. ## The unsubscribe page When a recipient opens their unsubscribe link, Emailit shows a hosted page and unsubscribes them right away. No confirmation click is needed. - **They leave every audience.** The contact's subscription is turned off in every audience it belongs to, not only the ones the campaign went to. The contact itself and its marketing status don't change. - **They can say why.** The page asks "Tell us why you're unsubscribing" with the options **Not interested**, **Too many emails**, **Never signed up**, **This is a spam** and **Other**. Answering is optional. - **They can undo it.** **Resubscribe** turns their subscription back on in every audience they belong to and shows "Resubscribe successful!". The page doesn't offer a preference center: people can't pick which audiences to stay on. If you need that, link to a page on your own site and update their subscriptions with the API. Each unsubscribe sends one `email.unsubscribed` event and a [`subscriber.updated`](/docs/webhooks/events/subscriber/) event for each audience. A resubscribe sends `email.resubscribed` and `subscriber.updated`. Receive them with [webhooks](/docs/webhooks/) to sync opt-outs to your own systems. See [Event types](/docs/webhooks/event-types/). ## Unsubscribes in campaign reports Unsubscribes made through the link are recorded against the campaign that sent it: - The **Unsubscribed** card on the campaign's **Overview** shows the count and the share of sent emails. - The **Unsubscribes** tab lists each one with **Email**, **First name**, **Last name**, **Reason**, **IP address** and **Unsubscribed At**. Search it, or filter by reason or date. - The PDF report includes an **Unsubscribe reasons** breakdown. The **Reason** column shows the reason recorded with the unsubscribe, when there is one. Recorded reasons appear as **Manual**, **Unsubscribe Link** or **Complaint**. Unsubscribes you make yourself in the dashboard or the API change the subscription, but they aren't linked to a campaign and don't appear in these reports. See [Campaign reports](/docs/campaigns/reports/). ## Complaints When a recipient marks a campaign email as spam and their mailbox provider reports it back, the email's status becomes **Complained** and, with the default automatic suppression settings, the address is added to your suppression list with type `complaint`. Campaigns skip suppressed addresses, so the person gets no further campaigns. See [Bounces and complaints](/docs/deliverability/bounces-and-complaints/). ## Manage opt-outs yourself ### In the dashboard | To | Do this | | --- | --- | | Unsubscribe or resubscribe someone in one audience | On the contact page, open the audience's row menu and select **Unsubscribe** or **Resubscribe**. Or on the audience page, select **Edit** and switch **Subscribed**. | | Stop all campaigns to some contacts | On **Email Marketing → Contacts**, select them and use **Actions > Unsubscribe**. **Resubscribe** reverses it. | | Find everyone who opted out | Filter the Contacts list by **Unsubscribed**, or filter an audience's subscribers by **Subscribed**. | | Stop every email to an address | Add it under **Email API → Suppressions** with type `recipient`. | ### With the API Unsubscribe from one audience by updating the subscriber. You can use the email address instead of the `sub_` ID: ```bash curl https://api.emailit.com/v2/audiences/aud_5hJ2kL8mNp4Qr/subscribers/ada@example.com \ -X POST \ -H "Authorization: Bearer $EMAILIT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "subscribed": false }' ``` Set the marketing status of up to 100 contacts at once with the `unsubscribe` or `resubscribe` [bulk action](/docs/api-reference/contacts/bulk/), or of one contact with `unsubscribed` on [Update a contact](/docs/api-reference/contacts/update/): ```bash curl https://api.emailit.com/v2/contacts/bulk \ -X POST \ -H "Authorization: Bearer $EMAILIT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "action": "unsubscribe", "ids": ["con_2kq8Vt4xLm7Rz", "con_9fW3pQ1nBc6Yt"] }' ``` To export your opt-outs, list contacts with `unsubscribed=true` or subscribers with `subscribed=false`. See [List contacts](/docs/api-reference/contacts/list/) and [List subscribers](/docs/api-reference/audiences/subscribers/list/). ## Transactional email and unsubscribes Unsubscribes and marketing status only control campaigns. Emails you send with the [Email API](/docs/email-api/) or [SMTP](/docs/smtp/), and emails sent by [automations](/docs/automations/), still go out to people who unsubscribed. That's intended: receipts, password resets and account notices must keep working. Keep it that way by sending only transactional content through those channels. A few things to know: - `{{unsubscribe_url}}` is a campaign merge tag. It isn't filled in for API, SMTP or automation emails, and those emails don't get the `List-Unsubscribe` headers automatically. - If you send marketing-style email outside campaigns, include your own unsubscribe link and honor it, for example by [adding a suppression](/docs/suppressions/manage/) when someone opts out. - To stop all email to an address, transactional included, add a suppression with type `recipient`. > **Tip:** Process opt-outs that reach you by other routes, such as a reply asking to be removed, the same day. Unsubscribing the contact or adding a suppression takes effect for the next campaign immediately. ## Related - [Merge tags](/docs/campaigns/merge-tags/): Every tag you can use in campaigns, including the unsubscribe link. - [Suppressions](/docs/suppressions/): Block an address from all sending. --- Source: https://emailit.com/docs/audiences/unsubscribes/ --- # Automations > Automations run steps such as sending an email, waiting or updating a contact when a trigger fires. Learn contexts, statuses, runs, credits and the API. An automation is a workflow that starts on its own: when a trigger fires, such as a contact joining an audience or an email bouncing, Emailit starts a run that works through the steps you've connected, such as sending an email, waiting a day or updating the contact. Use automations for welcome emails, onboarding sequences, birthday emails, forwarding and alerts. > **Automations is in beta:** Automations work in every workspace, but some options are still being finished. Where the dashboard and the API differ, these pages say so. ## How it works 1. **A trigger fires.** Every automation starts with one trigger, for example **Added to audience**. Optional filters narrow it down. 2. **Emailit starts a run** for the contact or email that caused it, and charges 3 credits. 3. **The run works through the steps** connected to the trigger, one after another. **Wait** steps pause the run, and **Condition** steps send it down a **Yes** or **No** branch. 4. **The run ends** when it reaches the last step of its branch, or fails if a step fails. You build the flow on a canvas in the dashboard, or send it to the API as a list of steps and connections. See [Triggers](/docs/automations/triggers/) and [Steps](/docs/automations/steps/). ## Contexts Each automation has a context, which decides what a run is about and which triggers and steps are available. You choose it when you create the automation, and it can't be changed later. | Context | Label | Each run is about | Good for | Triggers | | --- | --- | --- | --- | --- | | **Contact** | Easy | One contact | Welcome series, onboarding, re-engagement | Audience membership, contact changes, date anniversaries | | **Email** | Medium | One email | Reply handlers, auto-forwarders, bounce alerts | Email events, such as delivered, bounced or received | | **Event** | Advanced | One API call | API-driven flows, custom integrations | A manual trigger you call with the API | In the dashboard, the **Event** context shows a **Soon** badge and can't be selected yet. You can create Event automations with the API. ## Create an automation 1. **Start.** Go to **Email Marketing → Automations** and select **New automation**. 2. **Choose the context.** Pick **Contact** or **Email**. 3. **Choose a template.** Pick one of the [ready-made recipes](/docs/automations/recipes/) or **Start from scratch** for a blank canvas with just a trigger. 4. **Name it.** Enter a **Name** and an optional **Description**, then select **Create**. The automation opens as a draft. 5. **Build the flow.** On the **Editor** tab, select the trigger and each step to configure them, and add steps with the plus buttons. Select **Save**. 6. **Start it.** Select **Start**. From now on, every matching trigger starts a run. The list page has tabs for **Contact**, **Email** and **Event** automations and shows each one's **Name**, **Status**, **Last triggered** time and **Created** date. ## Statuses | Status | Triggers start runs | Editable in the dashboard | How to get there | | --- | --- | --- | --- | | **Draft** | No | Yes | Every new automation starts as a draft. | | **Running** | Yes | No | **Start** a draft or paused automation. | | **Paused** | No | Yes | **Pause** a running automation. **Start** resumes it. | | **Stopped** | No | No | Only with the API's [stop](/docs/api-reference/automations/stop/) endpoint, which also cancels every run in progress. | Pausing stops new runs from starting, but runs already in progress continue, including those waiting in a **Wait** step. **Delete**, in the menu at the top of the automation, removes it in any status. ### Editing rules The **Editor** tab is only available while the automation is a draft or paused. On a running automation, the tab shows **Pause to edit**. **Save** checks the whole flow and highlights the steps that need attention, for example a **Send email** step without a template or a wait longer than 30 days. Fix them and save again before you select **Start**: starting doesn't run the checks again. ## Runs A run is one pass through the automation for one contact, email or API call. Each run has a status: | Run status | Meaning | | --- | --- | | **Running** | The run is working through its steps or waiting. | | **Completed** | Every step on the run's path finished. | | **Failed** | A step failed, or the workspace didn't have enough credits when the run started. | | **Canceled** | The automation was stopped with the API while the run was in progress. | By default, every trigger starts a new run, even if the same contact or email already has one in progress. The same event never starts two runs of one automation. See [Runs and stats](/docs/automations/runs/) for the run history and debugging. ## Credits | Action | Credits | | --- | --- | | Each run | 3, charged when the run starts | | Each email sent by a **Send email** step | 1 | | Each email sent by a **Forward email** step | 1 | If the workspace doesn't have 3 credits when a trigger fires, the run is created with status **Failed** and the reason `insufficient_credits` in its `meta`. If credits run out in the middle of a run, the **Send email** or **Forward email** step fails. See [Credits](/docs/billing/credits/). ## Use the API The [Automations API](/docs/api-reference/automations/) manages automations and reads their runs. It needs an API key with **Full Access**. | Endpoint | Use it to | | --- | --- | | [Create](/docs/api-reference/automations/create/), [update](/docs/api-reference/automations/update/), [retrieve](/docs/api-reference/automations/get/), [list](/docs/api-reference/automations/list/) and [delete](/docs/api-reference/automations/delete/) | Manage automations. Steps and connections are sent together. | | [Start](/docs/api-reference/automations/start/), [pause](/docs/api-reference/automations/pause/) and [stop](/docs/api-reference/automations/stop/) | Change the status. | | [Trigger a run](/docs/api-reference/automations/trigger/) | Fire a **Manual** trigger, with an optional `payload`. The automation must be running. This is only possible with the API. | | [List runs](/docs/api-reference/automations/runs/), [retrieve a run](/docs/api-reference/automations/run/), [statistics](/docs/api-reference/automations/stats/) and [step statistics](/docs/api-reference/automations/step-stats/) | Read the run history and per-step results. | The API also accepts automation `settings` that the dashboard doesn't show yet: | Setting | Default | Effect | | --- | --- | --- | | `allow_reentry` | `true` | Set to `false` to skip new runs for a contact or email that already has a run in progress. | | `max_concurrent_runs` | `0` (no limit) | Skip new runs while this many runs are in progress. | | `cooldown_seconds` | `0` | Skip new runs for a contact or email that started a run within this many seconds. | | `on_step_failure` | `stop` | `stop` fails the run when a step fails. `skip` lets the run continue. | Skipped runs aren't created and don't use credits. ```bash curl https://api.emailit.com/v2/automations \ -X POST \ -H "Authorization: Bearer $EMAILIT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "context": "contact", "name": "Welcome email", "settings": { "allow_reentry": false }, "steps": [ { "key": "trigger-1", "type": "trigger", "trigger": "contact.added_to_audience", "config": { "audience_id": "aud_5hJ2kL8mNp4Qr" } }, { "key": "send_email-1", "type": "action", "action": "send_email", "config": { "type": "template", "template_id": "welcome" } } ], "connections": [ { "from": "trigger-1", "to": "send_email-1", "branch": "default" } ] }' ``` The automation is created as a draft. Call [Start an automation](/docs/api-reference/automations/start/) to turn it on. ## Next steps - [Triggers](/docs/automations/triggers/): Every trigger and its options. - [Steps](/docs/automations/steps/): Every step, its settings and how branches work. - [Recipes](/docs/automations/recipes/): The six ready-made templates. - [Runs and stats](/docs/automations/runs/): Follow runs and debug failures. --- Source: https://emailit.com/docs/automations/ --- # Automation recipes > The six ready-made automation templates, from welcome and birthday emails to bounce alerts and forwarding, with their steps and what to fill in. When you create an automation, you can start from one of six templates instead of a blank canvas. Each template sets up the trigger and steps for a common job. This page explains what each one does and what you need to fill in before you start it. ## Use a recipe 1. **Create the automation.** Go to **Email Marketing → Automations**, select **New automation**, pick the context and then the template. 2. **Name it** and select **Create**. The automation opens as a draft with the template's steps. 3. **Fill in the gaps.** Templates leave the email templates and addresses empty. Open the **Editor** tab, select each step and complete its settings, as described for each recipe below. 4. **Save and start.** Select **Save**, fix any highlighted steps, then select **Start**. Every recipe uses 3 credits per run, plus 1 credit for each email it sends or forwards. Create the email [templates](/docs/templates/) you'll need before you start. ## Contact recipes ### Thank you for subscribing Sends a welcome email when someone joins an audience. | Step | Setting in the template | What to do | | --- | --- | --- | | **Added to audience** | Any audience | Pick the **Audience** to welcome people to, or leave it empty for all audiences. | | **Send email** | Subject override "Thanks for subscribing!" | Pick the **Email template**. Change or clear the **Subject** override to use your own. | The welcome goes to people added from the dashboard, the API or the **Add to audience** bulk action. Imports and [subscribe URL](/docs/audiences/subscribe-url/) sign-ups don't start it. See [Added to audience](/docs/automations/triggers/#added-to-audience). ### Wishing Happy Birthday Sends a birthday email every year on each contact's birthday. | Step | Setting in the template | What to do | | --- | --- | --- | | **Date anniversary** | No field | Pick the **Date field** that holds birthdays, for example a Date custom field with the key `birthday`. | | **Send email** | Subject override "Happy Birthday!" | Pick the **Email template**. | You need a Date [custom field](/docs/contacts/custom-fields/) with birthdays filled in, in `YYYY-MM-DD` format. Runs start once a day, in UTC, for every contact whose date matches today's month and day. In the current beta, also set the trigger's `date_field` with the API, as described in [Date anniversary](/docs/automations/triggers/#date-anniversary). In the email template, use Temple to personalize it, for example `Happy birthday, {{first_name}}!`. ### Three day onboarding sequence Sends three emails over three days to people who join an audience. | Step | Setting in the template | What to do | | --- | --- | --- | | **Added to audience** | Any audience | Pick the **Audience**. | | **Wait / Delay** | 1 hour | Adjust if you want the first email sooner or later. | | **Send email** | No template | Pick the first email's template. | | **Wait / Delay** | 1 day | | | **Send email** | No template | Pick the second email's template. | | **Wait / Delay** | 1 day | | | **Send email** | No template | Pick the third email's template. | To stop the sequence for people who no longer qualify, add a **Condition** step before a send, for example on a custom field that marks customers who already upgraded, and connect only the **No** branch to the next email. ## Email recipes ### Prevent suppression of an email Keeps one important address deliverable: when an email to it is suppressed, the automation removes the address from the suppression list and forwards the suppressed email to it. | Step | Setting in the template | What to do | | --- | --- | --- | | **Email suppressed** | Filter: **To** **Equals** (empty) | Enter the address to protect as the filter value, for example `ops@acme.com`. Until you do, the automation never runs. | | **Remove from suppressions** | | Nothing to set. Removes the email's recipient from the suppression list. | | **Forward email** | **Forward to** empty | Enter where the copy should go, usually the same address. | The original email stays suppressed. The forward is a new email with the subject "Fwd: " plus the original subject. Only protect addresses you control: addresses are suppressed for a reason, such as bounces or complaints. See [Suppressions](/docs/suppressions/). ### Forward received email Forwards every inbound email to another address, for example your support inbox. | Step | Setting in the template | What to do | | --- | --- | --- | | **Email received** | No filter | Optionally add a filter, for example **To** **Equals** `support@inbound.acme.com`, to forward only some addresses. | | **Forward email** | **Forward to** empty | Enter the address to forward to. | You need [inbound email](/docs/inbound/set-up/) set up on a domain first. See [Forward with automations](/docs/inbound/forward-with-automations/). ### Notify me of any bounces Emails you whenever an outgoing email bounces. | Step | Setting in the template | What to do | | --- | --- | --- | | **Email bounced** | No filter | Add a filter to narrow it down, for example **From** **Equals** `billing@acme.com`. Also add **To** **Not equals** your alert address, so a bounced alert can't trigger another one. | | **Send email** | Subject override "An email bounced" | Pick the **Email template** and enter your address in **To (recipient)**. | In the alert template, use Temple to include details of the bounced email from the event, for example: ```html

The email "{{payload.object.subject}}" to {{payload.object.to}} bounced.

Email ID: {{payload.object.id}}

``` ## Related - [Triggers](/docs/automations/triggers/): Every trigger and its options. - [Steps](/docs/automations/steps/): Every step and its settings. --- Source: https://emailit.com/docs/automations/recipes/ --- # Automation runs and stats > Follow an automation's runs, read the overview and per-step statistics, debug failed runs, and fetch runs and stats with the API. Every time a trigger fires, the automation creates a run. This page shows where to see runs and their results, what the statistics on each step mean, how to find out why a run failed, and how to read the same data with the API. ## Overview tab Open an automation from **Email Marketing → Automations**. The **Overview** tab shows: - **Total executions**, **Completed**, **Failed** and **Running** counts. - A read-only diagram of the flow. Select any step to see its **Stats**. - **Edit**, which opens the editor while the automation is a draft or paused. While the automation is running, the numbers refresh every 10 seconds. ## Step stats Each step's **Stats** count how many runs reached it and what happened there. What you see depends on the step: | Step | Stats | | --- | --- | | **Send email** | A funnel of **Accepted**, **Delivered**, **Opened** and **Clicked**, each as a share of accepted emails, plus counts of **Bounced**, **Failed**, **Complained** and **Unsubscribed**. The counts follow each email after the step sent it. | | **Condition (If/Else)** | **Matched** (the run took **Yes**) and **Not matched** (it took **No**). | | Call webhook (API) | **2xx Success**, **4xx Client Error**, **5xx Server Error**, **Timeout** and **Network Error**. | | Random split (API) | Runs per variant. | | Every other step | Runs per status, such as **Completed**, **Waiting** and **Failed**. | Automation emails aren't tracked for loads and clicks, so **Opened** and **Clicked** stay at 0 for them. See [Send email](/docs/automations/steps/#send-email). ## Runs tab The **Runs** tab lists every run, newest first, with its **Status**, the **Event** that started it, and when it **Started** and **Completed**, with the **Duration**. Filter by status, event or creation date. Select a run to see its details: its status, start and end times, and each step it went through, in order, with the step's status and duration. Expand **Output data** on a step to see what it returned, such as the ID of the email it sent, the branch a condition took or the error that made it fail. | Run status | Meaning | | --- | --- | | **Running** | Working through its steps, or waiting in a **Wait** step. | | **Completed** | Every step on its path finished. | | **Failed** | A step failed, or the workspace didn't have enough credits to start the run. | | **Canceled** | The automation was stopped with the API while the run was in progress. | ## Debug a failed run 1. **Find the failed runs.** On the **Runs** tab, filter by **Status** `failed`. 2. **Open a run.** The failed step is marked in red. 3. **Read the error.** Expand **Output data** on the failed step. The `error` field says what went wrong, for example `send_email: Sending domain is not verified or not found`. See [When a step fails](/docs/automations/steps/#when-a-step-fails) for common errors and their fixes. 4. **Fix the cause.** For a step setting, select **Pause**, correct the step on the **Editor** tab, select **Save** and then **Start**. For a workspace problem, such as missing credits or an unverified domain, fix it under **Workspace** or **Email API**. A failed run with no steps usually failed because the workspace didn't have the 3 credits needed to start it. With the API, its `meta` shows `"failure_reason": "insufficient_credits"`. [Top up credits](/docs/billing/credits/) or turn on [auto-refill](/docs/billing/auto-refill/) so runs don't fail. Failed runs aren't retried. New triggers start new runs as usual. To run the automation again for a contact or email that failed, fire its trigger again, for example by removing the contact from the audience and adding it back. A run that stays **Running** for a long time is usually in a **Wait** step. Open it to see which step it's on. ## Use the API [List runs](/docs/api-reference/automations/runs/) returns an automation's runs, newest first. Filter with `filter[status]` (`running`, `completed`, `failed` or `canceled`) and page with `page` and `per_page` (up to 100): ```bash curl -G "https://api.emailit.com/v2/automations/aut_3Mv8Xq2nKp5Lt/runs" \ --data-urlencode "filter[status]=failed" \ --data-urlencode "per_page=25" \ -H "Authorization: Bearer $EMAILIT_API_KEY" ``` ```json { "data": [ { "id": "aur_7Kp2Vx9mQt4Lw", "automation_id": "aut_3Mv8Xq2nKp5Lt", "contact_id": "con_2kq8Vt4xLm7Rz", "email_id": null, "event_id": null, "event": "contact.added_to_audience", "payload": { "object": { "id": "sub_7Rt2vX9kLm3Qp", "object": "subscriber" } }, "meta": { "source_event_id": "evt_2Hn6Wq8rTc3Mz" }, "status": "failed", "started_at": "2026-10-01T09:30:00Z", "completed_at": "2026-10-01T09:30:02Z", "created_at": "2026-10-01T09:30:00Z", "updated_at": "2026-10-01T09:30:02Z" } ], "total_records": 1, "per_page": 25, "current_page": 1, "total_pages": 1 } ``` [Retrieve a run](/docs/api-reference/automations/run/) adds `run_steps`, with each step's `step_id`, `status`, `data` (the output, including any `error`), `started_at` and `completed_at`. [Retrieve statistics](/docs/api-reference/automations/stats/) returns the stats of every step, keyed by step key. Narrow them with `since` and `until` (ISO 8601) or `run_ids[]`. [Retrieve step statistics](/docs/api-reference/automations/step-stats/) returns one step's stats: ```json { "data": { "send_email-1": { "total": 120, "by_status": { "completed": 118, "failed": 2 }, "by_outcome": { "delivered": 110, "bounced": 3, "accepted": 5, "error": 2 }, "funnel": { "accepted": 115, "delivered": 110, "loaded": 0, "clicked": 0, "bounced": 3, "failed": 0, "complained": 0, "unsubscribed": 0 } } } } ``` `by_status` counts how the step ended in each run. `by_outcome` counts its results, such as the latest status of the email it sent or `matched` and `not_matched` for a condition. `funnel` appears for **Send email** steps. ## Related - [Steps](/docs/automations/steps/): Step settings and common errors. - [Automations overview](/docs/automations/): Statuses, credits and the API. --- Source: https://emailit.com/docs/automations/runs/ --- # Automation steps > Reference for every automation step, from send email, wait and condition to audience, contact and suppression steps, plus branches and step failures. Steps are what a run does after its trigger fires. This page lists every step by context with its settings and rules, explains how steps connect into branches, and shows what happens when a step fails. ## Add and configure steps On the automation's **Editor** tab (available while the automation is a draft or paused): - **Add a step:** select the plus button below the last step, or below **Yes** or **No** on a condition. Search the picker or pick from **Actions**. - **Configure a step:** select it on the canvas. Its settings open in a side panel. Once the step has run, a **Stats** tab appears next to **Configure**. - **Remove a step:** select it and select **Delete step** in the side panel. - **Save:** select **Save**. Steps with problems are highlighted with an error count. ## Steps by context | Step | API key | Contact | Email | Event (API) | | --- | --- | --- | --- | --- | | [Send email](#send-email) | `send_email` | Yes | Yes | Yes | | [Wait / Delay](#wait--delay) | `wait` | Yes | Yes | Yes | | [Condition (If/Else)](#condition-ifelse) | `condition` | Yes | Yes | Yes | | [Add to audience](#add-to-audience) | `add_to_audience` | Yes | | | | [Remove from audience](#remove-from-audience) | `remove_from_audience` | Yes | | | | [Edit contact](#edit-contact) | `edit_contact` | Yes | | | | [Forward email](#forward-email) | `forward_email` | | Yes | Yes | | [Add to suppressions](#add-to-suppressions) | `add_to_suppressions` | | Yes | Yes | | [Remove from suppressions](#remove-from-suppressions) | `remove_from_suppressions` | | Yes | Yes | | [Create contact](#create-contact) | `create_contact` | | Yes | Yes | | [API-only steps](#api-only-steps) | `call_webhook`, `run_automation`, `experiment`, `end` | API | API | API | Step settings can include placeholders such as `{{contact.first_name}}` or `{{payload.object.to}}`, filled in for each run. See [Data available to steps](/docs/automations/triggers/#data-available-to-steps). ## Send email Sends an email built from one of your [templates](/docs/templates/). | Setting | Required | Notes | | --- | --- | --- | | **Email template** | Yes | The template to send. With the API, `template_id` takes a `tem_` ID or an alias, which uses the published version. | | **To (recipient)** | Email and Event contexts | The address to send to. Contact automations always send to the run's contact. | | **Subject** | No | Overrides the template's subject. | | **From** | No | Overrides the template's sender, for example `"Acme" `. | | **Reply to** | No | Overrides the template's reply-to address. | Rules: - **The template is rendered with [Temple](/docs/templates/temple/).** In Contact automations, the contact's fields are available directly, so `{{first_name}}`, `{{email}}` and `{{custom_fields.plan}}` work. Every context also has `{{contact.*}}`, `{{payload.*}}` and `{{meta.*}}`. Campaign merge tags such as `{{cf.plan}}` and `{{unsubscribe_url}}` aren't filled in. - **The sender must be on a verified domain.** The **From** override or the template's sender must use a domain verified in the workspace. **Save** checks overrides, and the step fails at run time if the domain isn't verified. - **A subject and content are required**, from the template or the overrides. - **Each email costs 1 credit**, on top of the 3 credits for the run. - **It doesn't check subscriptions.** The email is sent even if the contact unsubscribed from your audiences. Suppressed addresses are still blocked at delivery, and sandbox workspaces can only send to members' account addresses. - **No unsubscribe link or loads and clicks.** Automation emails don't get `List-Unsubscribe` headers, and loads and clicks aren't tracked. The step's **Stats** follow each email it sent through **Accepted** and **Delivered**, and count bounces, failures and complaints. ## Wait / Delay Pauses the run before the next step. | Setting | Notes | | --- | --- | | **Duration** and **Unit** | **Minutes**, **Hours** or **Days**. The longest wait is 30 days. With the API, set `seconds`, up to `2592000`. | While a run waits, it stays **Running** and the wait step shows as waiting. Pausing the automation doesn't interrupt waiting runs. ## Condition (If/Else) Sends the run down the **Yes** branch when the rules match, or the **No** branch when they don't. - **Rules:** a field, an operator and a value, combined with **All rules match** or **Any rule matches**. The operators are the same as for [trigger filters](/docs/automations/triggers/#filters). - **Fields:** in Contact automations, **Email**, **First name**, **Last name** and your custom fields. In Email automations, the fields of the trigger's event. With the API, `field` can be any path, with the prefixes `contact.`, `email.`, `payload.` and `meta.`. - **Branches:** add the next step under **Yes**, **No** or both. A branch with no step ends the run there. ## Add to audience Contact context. Subscribes the run's contact to the **Audience** you choose. If the contact had unsubscribed from it, it's subscribed again. The step fails if the audience has reached its [subscriber limit](/docs/audiences/#limits). It doesn't start **Added to audience** automations. ## Remove from audience Contact context. Turns the contact's subscription to the **Audience** off. The contact stays on the audience as an unsubscribed subscriber, unlike **Remove from audience** in the dashboard, which deletes the membership. ## Edit contact Contact context. Sets one or more fields on the run's contact. Add a row per field, with a **Field name** and a **Value**: | Field name | Effect | | --- | --- | | `first_name`, `last_name`, `email` | Updates that field. | | `unsubscribed` | Sets the contact's marketing status. Use `true` to unsubscribe the contact from all campaigns. Any non-empty value counts as `true`. | | Any other name | Stores the value in the custom field with that key, keeping the other custom fields. | Values can use placeholders, for example `{{payload.object.audience.name}}`. The change doesn't send a `contact.updated` event, so it doesn't start **Contact updated** automations or webhooks. ## Forward email Email and Event contexts. Sends a copy of the run's email, with its original content and attachments, to the address in **Forward to**. The subject becomes "Fwd: " plus the original subject. The email must come from a verified sending domain in the workspace, and each forward costs 1 credit. See [Forward with automations](/docs/inbound/forward-with-automations/). ## Add to suppressions Email and Event contexts. Adds an address to your [suppression list](/docs/suppressions/) with type `recipient`, which blocks all sending to it, and the **Reason** you enter (default `automation`). In Email automations, the address is the email's recipient. In Event automations, set `email` with the API, for example `{{payload.email}}`. The step sends a `suppression.created` event. ## Remove from suppressions Email and Event contexts. Removes the address from the suppression list: the email's recipient in Email automations, or `email` set with the API in Event automations. ## Create contact Email and Event contexts. Creates a contact and, if you pick an **Audience (optional)**, subscribes it there. - In Email automations, the contact is the email's recipient address. For received emails, that's the address the email was sent to. To create a contact for the sender instead, set `email` to `{{email.mail_from}}` with the API. - In Event automations, set `email` and optionally `first_name` with the API. - If the contact already exists, it's kept as it is and only subscribed to the audience. - The step doesn't start **Added to audience** automations. ## API-only steps These steps can be added with the API but not in the dashboard editor yet. They work in every context. | Step | API key | Config | What it does | | --- | --- | --- | --- | | Call webhook | `call_webhook` | `url` (required), `method` (default `POST`), `headers`, `body` | Sends an HTTP request with a JSON content type and records the response. A non-2xx response or a timeout is recorded as the step's outcome (`2xx`, `4xx`, `5xx`, `timeout` or `network_error`) and doesn't fail the run. | | Run automation | `run_automation` | `automation_id` (required) | Starts a run of another running automation with the same payload, then continues. The other run costs its own 3 credits. Skipped if that automation isn't running. | | Random split | `experiment` | `variants`: list of `{ "key", "weight" }`, optional `control` | Sends each run down one branch at random, in proportion to the weights. Each variant's branch is named after its `key`. When a variant's branch ends, the run continues on the step's `default` branch. | | End | `end` | None | Marks the end of a branch. | ```json { "key": "notify-crm", "type": "action", "action": "call_webhook", "config": { "url": "https://crm.acme.com/hooks/new-subscriber", "method": "POST", "headers": { "Authorization": "Bearer crm_token" }, "body": { "email": "{{contact.email}}", "source": "emailit" } } } ``` ## Branches and connections With the API, an automation is a list of `steps`, each with a unique `key`, and a list of `connections` from one step key to the next: ```json { "steps": [ { "key": "trigger-1", "type": "trigger", "trigger": "contact.added_to_audience", "config": {} }, { "key": "is-pro", "type": "action", "action": "condition", "config": { "filter": { "match": "all", "rules": [{ "field": "custom_fields.plan", "operator": "equals", "value": "pro" }] } } }, { "key": "send-pro", "type": "action", "action": "send_email", "config": { "type": "template", "template_id": "welcome-pro" } }, { "key": "send-free", "type": "action", "action": "send_email", "config": { "type": "template", "template_id": "welcome-free" } } ], "connections": [ { "from": "trigger-1", "to": "is-pro", "branch": "default" }, { "from": "is-pro", "to": "send-pro", "branch": "yes" }, { "from": "is-pro", "to": "send-free", "branch": "no" } ] } ``` - **`branch`** is `default` for a normal next step, `yes` or `no` after a condition, and a variant key after a random split. - **A condition only follows the branch it picked.** A `default` connection out of a condition is never followed. - **A step with several outgoing connections on the same branch** starts all of them, and the branches run side by side. - **Every step must be reachable from a trigger.** In Contact and Email automations with several triggers, all triggers must connect to the same first step. - **Send `steps` and `connections` together** when you update an automation. Steps whose keys don't change keep their history and stats. ## When a step fails By default, a failed step fails the whole run, and the remaining steps don't run. The error is saved on the step, and you can read it in the run's details on the **Runs** tab. Set `on_step_failure` to `skip` in the automation's `settings` with the API to let runs continue past failed steps instead. | Error | Cause | | --- | --- | | `send_email: Template '…' not found or not published` | The template was deleted, or the alias has no published version. | | `send_email: Sending domain is not verified or not found` | The sender's domain isn't verified in the workspace. | | `send_email: Unable to determine recipient address` | **To (recipient)** is empty, or the contact no longer exists. | | `send_email: Insufficient credits (…)` | The workspace ran out of credits. | | `Pro includes 50,000 subscribers per audience.` (or your plan's limit) | An **Add to audience** or **Create contact** step hit a full audience. | | `forward_email: Source email has no raw content to forward` | The email's content was already removed by your [data retention](/docs/data-retention/) settings. | See [Runs and stats](/docs/automations/runs/#debug-a-failed-run) for how to find and fix failed runs. ## Related - [Triggers](/docs/automations/triggers/): What starts a run. - [Recipes](/docs/automations/recipes/): Ready-made automations to start from. --- Source: https://emailit.com/docs/automations/steps/ --- # Automation triggers > Reference for every automation trigger by context, with their options and filters, what fires them, and the trigger keys to use with the API. A trigger decides when an automation starts a run. This page lists every trigger available in each [context](/docs/automations/#contexts), what fires it, its options, and the key you use for it in the API. ## How triggers work - **One trigger per automation in the dashboard.** Select the trigger on the canvas and change it with **Trigger type**. With the API, Contact and Email automations can have several triggers, as long as they all connect to the same first step. Event automations have exactly one. - **The automation must be running.** Triggers in draft, paused or stopped automations are ignored. Events from before you start an automation don't start runs later. - **Runs start within seconds.** Emailit picks up new events every few seconds. ### Filters Contact updated and every email trigger take an optional filter, under **Filter events (optional)**. Each rule compares one field of the event with a value: - **Operators:** **Equals**, **Not equals**, **Contains**, **Not contains**, **Greater than**, **Less than**, **Is set**, **Is not set**, **In**, **Not in**, **Starts with** and **Ends with**. **Greater than** and **Less than** compare numbers. The rest compare text and are case-sensitive. - **Match mode:** **All rules match** or **Any rule matches**. With the API, a filter is `{ "match": "all", "rules": [{ "field": "...", "operator": "equals", "value": "..." }] }` in the trigger's `config.filter`, with `match` set to `all` or `any`. Fields are paths into the event's object, for example `to` or `link.url`. ## Contact triggers | Trigger | API key | Options | Starts a run when | | --- | --- | --- | --- | | **Added to audience** | `contact.added_to_audience` | **Audience**. Leave it empty for any audience. | A contact joins the audience, or is added back after unsubscribing. | | **Removed from audience** | `contact.removed_from_audience` | **Audience**. Leave it empty for any audience. | A contact's membership in the audience is deleted. | | **Contact updated** | `contact.updated` | Optional filter | A contact's email, names, custom fields or marketing status change. | | **Date anniversary** | `contact.date_anniversary` | **Date field** | Once a year, on the month and day stored in a date custom field. | ### Added to audience Fires when someone is added to an audience from the dashboard (**Add subscriber**, **Add to audience**, **Add contact** with audiences), with the API ([Add a subscriber](/docs/api-reference/audiences/subscribers/add/), or [Create a contact](/docs/api-reference/contacts/create/) with `audiences`), or with the **Add to audience** bulk action. Adding back someone who unsubscribed also fires it. It doesn't fire for contacts added by a [file import](/docs/contacts/import-export/), a [subscribe URL](/docs/audiences/subscribe-url/) sign-up, or another automation's **Add to audience** or **Create contact** step, and turning **Subscribed** back on for an existing subscriber doesn't count either. ### Removed from audience Fires when a subscriber is deleted: **Delete** on the audience page, **Remove from audience**, [Delete a subscriber](/docs/api-reference/audiences/subscribers/delete/), or a contact update whose `audiences` list leaves the audience out. Deleting a contact fires it once for each audience the contact was on. Unsubscribing doesn't fire it, because the person stays on the audience. ### Contact updated Fires whenever a contact is updated in the dashboard or with the API, including the **Unsubscribe** and **Resubscribe** bulk actions. The filter can check the current **Email**, **First name**, **Last name**, **Unsubscribed** and custom fields, and their previous values, listed as **Previous email**, **Previous first name** and so on. Previous values are only present for the fields that changed. For example, to react when a contact moves to the `pro` plan, add two rules with **All rules match**: `custom_fields.plan` **Equals** `pro`, and **Previous plan** (`previous.custom_fields.plan`) **Not equals** `pro`. ### Date anniversary Pick a **Date field**, a [custom field](/docs/contacts/custom-fields/) of type Date such as a birthday. Once a day, Emailit starts a run for every contact whose date has today's month and day, in UTC. The year doesn't matter, so a contact with `1990-04-12` gets a run every April 12. Each automation handles up to 10,000 contacts per day. > **Set the date field with the API:** In the current beta, the daily check reads the trigger's `date_field` setting, which the dashboard's **Date field** picker doesn't set yet. If your anniversary automation doesn't start runs, set it with [Update an automation](/docs/api-reference/automations/update/): give the trigger `"config": { "date_field": "birthday" }`, using the custom field's key without a prefix. ### API-only contact triggers | API key | Starts a run when | | --- | --- | | `contact.loaded_email` | A contact loads a tracked email sent to their address. | | `contact.clicked_in_email` | A contact clicks a tracked link in an email sent to their address. | | `contact.on_date` | A contact's date field, set in `config.date_field`, equals today's date in UTC. Fires once, not every year. | The API also accepts `contact.visits_url`, `contact.on_purchase` and `contact.on_event`, but nothing fires them yet. ## Email triggers Email triggers fire for emails in your workspace: everything you send with the API or SMTP, campaign and automation emails, and inbound email for **Email received**. Each run is about one email. | Trigger | API key | Starts a run when | Filter fields | | --- | --- | --- | --- | | **Email delivered** | `email.delivered` | The recipient's server accepted the email. | From, To, Subject, Status | | **Email bounced** | `email.bounced` | The email failed permanently. | From, To, Subject, Status | | **Email failed** | `email.failed` | The email couldn't be sent because of an error. | From, To, Subject, Status | | **Email suppressed** | `email.suppressed` | The email wasn't sent because the recipient is suppressed. | From, To, Subject, Status | | **Email complained** | `email.complained` | The recipient reported the email as spam. | From, To, Subject, Status | | **Email received** | `email.received` | An inbound email arrived. See [Inbound](/docs/inbound/). | From, To, Subject | | **Email loaded** | `email.loaded` | The recipient loaded a tracked email. | Recipient, Sender, Subject, IP address, User agent | | **Email clicked** | `email.clicked` | The recipient clicked a tracked link. | Recipient, Sender, Subject, Link URL, IP address, User agent | The editor also lists **Email accepted**, **Email scheduled**, **Email attempted** and **Email rejected**. Automations with these triggers can't be saved yet, so pick one of the triggers above. With the API you can also use `email.canceled`, which fires when a scheduled or queued email is canceled. > **Avoid loops:** Emails sent by automations fire email triggers too. An automation that sends an email whenever an email bounces would also run for its own notification if that bounced. Add a filter, for example **To** **Not equals** your alert address, so an automation can't trigger itself. ## Event triggers Event automations can only be created with the API for now. | Trigger | API key | Starts a run when | | --- | --- | --- | | **Manual trigger** | `system.manual` | You call [Trigger a run](/docs/api-reference/automations/trigger/). | | Schedule | `system.schedule` | Reserved. Nothing fires it yet, so call the trigger endpoint from your own scheduler, such as a cron job, instead. | ### Manual trigger Call the trigger endpoint of a running automation, with an optional `payload` object: ```bash curl https://api.emailit.com/v2/automations/aut_3Mv8Xq2nKp5Lt/trigger \ -X POST \ -H "Authorization: Bearer $EMAILIT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "payload": { "email": "ada@example.com", "plan": "pro" } }' ``` The endpoint returns `{ "message": "Automation trigger dispatched." }`, or `422` if the automation isn't running. Steps can read the payload as `{{payload.email}}`, `{{payload.plan}}` and so on. Emailit adds `automation_id` to the payload. > **One call reaches every manual automation:** In the current beta, a call to the trigger endpoint starts a run in every running automation of the workspace whose trigger is **Manual trigger**, not only the one in the URL. If you have more than one, make each one check its own ID first with a **Condition** step on `payload.automation_id`. `system.manual` also works as a trigger in Contact and Email automations created with the API. Include `contact_id` (a `con_` ID) or `email_id` in the payload to run the automation for that contact or email. ## Data available to steps Step settings, such as the recipient of **Send email** or the values of **Edit contact**, can include placeholders that are filled in for each run: | Placeholder | Contains | | --- | --- | | `{{contact.}}` | The run's contact in Contact automations, for example `{{contact.email}}` or `{{contact.custom_fields.plan}}`. | | `{{email.}}` | The run's email in Email automations, for example `{{email.rcpt_to}}` or `{{email.subject}}`. | | `{{payload.}}` | The event that started the run. For webhook-style events, the event's data is under `payload.object`, for example `{{payload.object.to}}`. For manual triggers, it's your `payload`. | | `{{meta.}}` | Extra data Emailit stores about the run. | Email templates sent by **Send email** use [Temple](/docs/templates/temple/) with the same data. See [Steps](/docs/automations/steps/#send-email). ## Related - [Steps](/docs/automations/steps/): What a run can do once it starts. - [Webhook event types](/docs/webhooks/event-types/): The events behind contact and email triggers. --- Source: https://emailit.com/docs/automations/triggers/ --- # Add-ons and services > Extend any Emailit plan with the Data retention add-on, dedicated IPs, a deliverability consultation, or the non-profit and student discount. Add-ons extend a workspace beyond its plan. Some you activate yourself, others you request from the Emailit team. This page covers each one and how to get it. ## At a glance | Add-on | Price | How to get it | | --- | --- | --- | | Data retention | $50 per month per workspace | Activate in **Workspace → Billing** | | Dedicated IP | See [pricing](/pricing/) | Request in **Workspace → Billing** | | Deliverability consultation | See [pricing](/pricing/) | Book from the [pricing page](/pricing/) | | Non-profit and student discount | 50% off credit purchases | Apply from the [pricing page](/pricing/) | Add-ons apply to one workspace. Only admins can activate the Data retention add-on, while anyone in the workspace can request a dedicated IP. ## Data retention Every plan keeps email data for a fixed time. The Data retention add-on lets you choose your own window for each data type, from 1 to 365 days. Use it to keep data longer than your plan does, or to delete it sooner for privacy. | | Pay as you go | Pro | Business | Custom | | --- | --- | --- | --- | --- | | Message contents kept | 7 days | 30 days | 30 days | Flexible | | Message activity kept | 30 days | 365 days | 365 days | Flexible | | Request logs kept | 7 days | 30 days | 30 days | Flexible | | Analytics kept | 365 days | Forever | Forever | Flexible | 1. **Activate the add-on.** Go to **Workspace → Billing**. Under **Add-ons**, select **Activate** on the **Data retention** row and complete Stripe Checkout. The row then shows **Active**. 2. **Set your windows.** Go to **Workspace → Settings → Data retention**. For **Message contents**, **Message activity**, **Logs** and **Analytics**, choose **Plan default** or 1, 3, 7, 14, 30, 60, 90, 180 or 365 days. 3. **Save.** Select **Save**. Emailit applies the new windows in its daily cleanup. The add-on is a monthly subscription paid through Stripe. If it ends, your plan defaults apply again. The add-on also comes with some [AppSumo](/docs/billing/appsumo/) purchases. See [Data retention](/docs/data-retention/) for what each data type covers. ## Dedicated IPs By default, mail goes out through Emailit's shared IP pools. A dedicated IP gives you a sending reputation that only your mail affects. It suits steady volume, and the pricing page recommends it from about 1,000 emails a day. 1. **Open the request form.** Go to **Workspace → Billing**. Under **Add-ons**, select **Contact sales** on the **Dedicated IP** row. 2. **Describe your needs.** Choose **Company size**, **Annual revenue**, **Annual email volume** and **How many dedicated IPs**, and fill in **Describe your needs**. 3. **Follow up.** Submit the form. The request opens under **Workspace → Settings → Requests**, where the team replies. A new IP has no reputation yet and needs to be warmed up. See [Dedicated IPs](/docs/deliverability/dedicated-ips/), [Warm-up](/docs/deliverability/warm-up/) and [Workspace requests](/docs/workspaces/requests/). ## Deliverability consultation A one-time, prepared 45-minute session with the Emailit deliverability team, based on a detailed review of your setup. It's useful before a large launch or when mail lands in spam. Book it from the [pricing page](/pricing/), where the current price is listed. ## Non-profit and student discount Qualifying non-profits and students get 50% off credit purchases. Apply from the [pricing page](/pricing/), or email [support@emailit.com](mailto:support@emailit.com) and tell us about your project. ## Moving from another provider? The [priority migration service](/docs/programs/priority-migration/) moves your domains, templates, suppressions and webhooks with help from an Emailit engineer. ## Related - [Plans](/docs/billing/plans/) - [Data retention](/docs/data-retention/) - [Dedicated IPs](/docs/deliverability/dedicated-ips/) --- Source: https://emailit.com/docs/billing/add-ons/ --- # AppSumo licenses > Redeem an Emailit AppSumo license, see what each tier includes, how it stacks with your plan and bonuses, and what upgrades, downgrades and refunds do. If you bought Emailit on AppSumo, your license adds monthly credits and sets domain and member limits for one workspace. This page explains each tier, how to redeem a license, and how it interacts with Emailit plans. ## Tiers | Tier | Sending domains | Workspace members | Monthly included credits | One-time bonus credits | | --- | --- | --- | --- | --- | | 1 | 1 | 1 | 20,000 | — | | 2 | 2 | 2 | 50,000 | — | | 3 | Unlimited | 3 | 100,000 | — | | 4 | Unlimited | 4 | 250,000 | — | | 5 | Unlimited | 5 | 1,000,000 | — | | 6 | Unlimited | 6 | 2,000,000 | 2,000,000 | A license applies to one workspace. On that workspace's **Workspace → Billing** page, the **AppSumo license** card shows your current tier, the start of the activated license key and how the tiers compare. ## How a license combines with your plan The license adds its features on top of whichever plan the workspace is on. You can still upgrade to Pro or Business, or stay on Pay as you go. - **Credits stack.** Monthly included credits are your plan's credits, plus the tier's credits, plus any partner or referral bonus. For example, tier 3 on Pro gives 100,000 + 100,000 = 200,000 included credits a month. - **Domain and member limits come from the tier.** They replace your plan's limits, even when the plan would allow more. A tier 1 workspace on Pro can have 1 sending domain, not 100. - **Everything else comes from the plan.** Webhook endpoints, subscribers per audience, analytics, DMARC reports, retention and the extra-credit rate follow your plan. See [Plans](/docs/billing/plans/). - **Bonus credits never expire.** The tier 6 bonus is added as purchased credits. - **Included credits reset monthly** at 00:00 UTC on the 1st, or on your billing cycle if you also have a monthly Pro or Business subscription. See [Credits](/docs/billing/credits/#when-included-credits-reset). Pending email invitations count toward the member limit. See [Members and roles](/docs/workspaces/members-and-roles/#member-limits). ## Redeem a license 1. **Open the activation link.** After you buy, AppSumo sends you to `https://dash.emailit.com/appsumo?code=…`. Emailit fetches your license from AppSumo. 2. **Sign in or create an account.** Enter your email and select **Continue**. If you have an account, enter your password and finish sign-in with your emailed code. If you're new, enter **Name**, **Password** and **Confirm password**. 3. **Choose the workspace.** Pick a workspace under **Choose workspace to apply license**. Only workspaces without an AppSumo license are listed. 4. **Apply.** Select **Continue**. You see "License was successfully applied to selected workspace." The new limits and credits take effect immediately. If your account uses an authenticator app, the activation page can't finish sign-in. You see "This account has two-factor authentication enabled. Please sign in normally first, then open this link again." Sign in at [dash.emailit.com](https://dash.emailit.com), then open the activation link again. To put the license on a new workspace, create the workspace in the dashboard first, then open the activation link from AppSumo again. A license can be redeemed once. Opening the activation link for a license that's already in use signs in its owner and opens the dashboard. ## Upgrade, downgrade or refund You change tiers on AppSumo. Emailit receives the change and updates the workspace automatically: | Change | What happens in Emailit | | --- | --- | | Upgrade | The new tier's limits and monthly credits apply. Upgrading to tier 6 adds the 2,000,000 bonus credits. | | Downgrade | The lower tier's limits and credits apply. Leaving tier 6 removes the 2,000,000 bonus credits, even if you've already used them. | | Refund or deactivation | The license is removed from the workspace, with its credits, limits and any data retention add-on. Leaving tier 6 this way also removes the bonus credits. | If a downgrade leaves you with more domains or members than the new tier allows, existing ones keep working, but you can't add more. ## Data retention license AppSumo also sells a data retention add-on for Emailit licenses. When it's attached to your license, the workspace gets the [Data retention add-on](/docs/billing/add-ons/#data-retention) without a monthly charge. Choose your windows under **Workspace → Settings → Data retention**. If the add-on is refunded, the plan's default retention applies again. ## Troubleshooting | Message | What to do | | --- | --- | | License was already redeemed | The license is on another workspace. Sign in with the account that redeemed it, or contact support. | | License is not active | The license was refunded or deactivated on AppSumo. Check your AppSumo account. | | License or workspace was not found | Open the newest activation link from AppSumo, or contact support. | | This AppSumo license includes N workspace member(s). | Remove a member or cancel a pending invitation, or upgrade your tier. | | This license includes N domain(s). | Delete a domain you don't use, or upgrade your tier. | For anything else, email [support@emailit.com](mailto:support@emailit.com) with the first characters of your license key. ## Related - [Credits](/docs/billing/credits/) - [Plans](/docs/billing/plans/) - [Members and roles](/docs/workspaces/members-and-roles/) --- Source: https://emailit.com/docs/billing/appsumo/ --- # Auto-refill and credit alerts > Have Emailit buy credits automatically when your balance runs low, choose who gets refill, low-balance and invoice emails, and understand the refill rules. Auto-refill buys credits for you when a workspace's balance drops to a threshold, so sending never stops because you forgot to top up. The same card also controls the low-balance alert and who receives billing emails. This page explains every setting. ## Before you begin - You're an **Admin** of the workspace. - The workspace has a default card. Add one under **Payment method** on **Workspace → Billing**. See [Invoices and billing details](/docs/billing/invoices/#payment-methods). ## Turn on auto-refill 1. **Open the Auto-refill card.** Go to **Workspace → Billing**. The **Auto-refill** card is on the **Workspace plan** tab. 2. **Set the refill amount.** Use **Refill amount** to pick a whole-dollar amount from $20 to $500. The control shows how many credits that buys on your plan. 3. **Set the threshold.** Enter the **Minimum credit threshold**. When your balance is at or below this number, Emailit refills. 4. **Choose who's notified.** Review the email switches and enter **Notification emails**, separated by commas. 5. **Save and switch it on.** Select **Save refill settings**, then turn on the switch at the top of the card. The switch saves as soon as you flip it. ## Settings and defaults | Setting | Default | What it does | | --- | --- | --- | | Auto-refill switch | Off | Turns automatic purchases on or off. | | **Refill amount** | $20 | Charged per refill. $20 to $500, whole dollars. Credits are added at your plan's rate. | | **Minimum credit threshold** | 10,000 | Refill when the balance is at or below this many credits. | | **Email when a refill is about to happen** | On | Sends "Emailit is about to auto-refill your credits" before the charge. | | **Email when a refill happens** | On | Sends "Emailit auto-refilled your credits" with the amount charged and credits added. | | **Alert when running out of credits** | On | Sends "Emailit credits are running low" when the balance reaches the running-out threshold. Works even with auto-refill off. | | **Running-out threshold** | 1,000 | Balance at which the running-out alert is sent. | | **Email the same addresses when a new invoice is issued** | On | Sends "New Emailit invoice" with a link when an invoice is paid. | | **Notification emails** | Empty | Who gets all of the emails above. If empty, they go to the workspace's billing email. | The balance Emailit checks is the same as **Credits remaining** on the **Billable usage** tab: included credits left this month plus purchased credits. ## How refills work - **Checks run in the background.** Emailit checks balances regularly, not on every email, so set the threshold high enough to cover your sending between checks. - **One refill per hour.** After a refill, the next one can happen an hour later at the earliest. If you send more than one refill's worth of credits in an hour, raise the **Refill amount**. - **The default card is charged.** The charge uses the workspace's default card without a checkout page. If there's no default card, nothing is charged. - **Purchased credits.** Refilled credits are purchased credits. They never expire and count toward the [first credit purchase](/docs/billing/credits/#buy-credits) rule on Pay as you go. - **Failed charges.** If the card is declined, no credits are added. Update the card under **Payment method**. ## How alerts work The running-out alert is sent at most once every 24 hours per workspace, so a low balance doesn't flood your inbox. Set the **Running-out threshold** above your typical daily usage to get a day's warning. With auto-refill on, a sensible setup is a running-out threshold below the refill threshold. You then only get the alert if a refill fails or can't keep up. ## Example A Pro workspace sends about 20,000 emails an hour at peak. With a **Refill amount** of $20 it gets 200,000 credits per refill, which covers about ten hours of peak sending, so one refill an hour is plenty. A **Minimum credit threshold** of 50,000 leaves room for sending between balance checks. ## Turn auto-refill off Turn off the switch at the top of the **Auto-refill** card. Credits you already bought stay in the workspace. ## Related - [Credits](/docs/billing/credits/) - [Notification emails](/docs/account/notification-emails/) - [Invoices and billing details](/docs/billing/invoices/) --- Source: https://emailit.com/docs/billing/auto-refill/ --- # Credits > How Emailit credits work: what each action costs, included vs purchased credits, monthly resets, buying credits, the Billable usage tab and running out. Credits are the single currency for everything you do in Emailit. This page explains what uses credits, which credits are spent first, when they reset, and what happens when a workspace runs out. ## What each action costs | Action | Credits | | --- | --- | | Email sent with the API or SMTP (per recipient) | 1 | | Inbound email received | 1 | | Campaign email (per recipient) | 2 | | Automation run | 3 | | Email verification (per address) | 5 | - **API and SMTP email** costs 1 credit per recipient. A message to 3 addresses across `to`, `cc` and `bcc` costs 3 credits. - **Inbound email** costs 1 credit for each message received on your inbound domain. - **Campaign email** costs 2 credits per recipient. - **Automation runs** cost 3 credits each. Emails a run sends are charged on top, like any other email. - **Email verification** costs 5 credits per address. Verification lists are charged up front for every unique address in the list. Retrying or forwarding an email creates a new email and costs credits again. ## Your balance A workspace has two pools of credits: | Pool | Where it comes from | Expires | | --- | --- | --- | | **Included** | Your plan, plus any partner, referral or AppSumo bonus | Resets every month. Unused credits don't roll over. | | **Purchased** | Credits you buy, auto-refills, claimed referral rewards and some one-time bonuses | Never | Emailit always spends included credits first and only uses purchased credits once the month's included credits are gone. A single action can be split across both pools. | | Pay as you go | Pro | Business | Custom | | --- | --- | --- | --- | --- | | Included credits / month | Up to 3,000 | 100,000 | 2,500,000 | Flexible | ### Included credit sources Included credits can stack from several sources. On the **Billable usage** tab, hover the info icon next to **Workspace Included** to see them: | Source | Amount | | --- | --- | | **Plan included** | The plan's monthly credits | | **Partner** | +5,000 a month, or +10,000 on Pro and Business, if you signed up with a partner code | | **Referral** | +5,000 a month, or +10,000 on Pro and Business, if you signed up with a referral code | | **AppSumo plan included** | The monthly credits of your [AppSumo license](/docs/billing/appsumo/) tier | | **License included** | Extra credits agreed for your workspace, for example on a Custom plan | Partner and referral bonuses only apply to the first workspace the referred person creates. ## When included credits reset | Your workspace | Reset | | --- | --- | | Monthly Pro or Business subscription | When the monthly renewal invoice is paid, at the start of each billing cycle | | Everyone else: Pay as you go, yearly subscriptions and AppSumo-only workspaces | At 00:00 UTC on the 1st of each month | Purchased credits are never reset. ## Buy credits 1. **Open Billable usage.** Go to **Workspace → Billing** and select the **Billable usage** tab. You need the Admin role. 2. **Choose an amount.** Use the slider or type a whole-dollar amount from $20 to $500. The control shows how many credits you get at your plan's rate. 3. **Purchase.** Select **Purchase $20** (the button shows your amount), check it in the **Purchase email credits** dialog and select **Buy $20 of credits** to open Stripe Checkout. 4. **Pay.** Complete the payment. The credits are added to the workspace as soon as Stripe confirms it. You can also select **Top up** under the **Credits** meter in the sidebar, or on the **Credits & Pricing** card on the Dashboard. | Plan | Credits per $1 | Price per 10,000 | $20 buys | $500 buys | | --- | --- | --- | --- | --- | | Pay as you go | 5,000 | $2 | 100,000 | 2,500,000 | | Pro and Business | 10,000 | $1 | 200,000 | 5,000,000 | The rate is set when you buy. Credits you bought on Pay as you go keep their count when you upgrade. On Pay as you go, your first purchase also raises the sending domain limit from 3 to 25. To buy automatically when your balance runs low, turn on [auto-refill](/docs/billing/auto-refill/). ## The Billable usage tab | Element | What it shows | | --- | --- | | **Show $ balance** | Shows amounts in dollars at your plan's rate instead of credits. Only the view changes. Charges stay in credits. | | **Workspace Included** | This billing cycle's included credits, with their sources | | **Purchased credits** | Purchased credits left in the workspace | | **Total billing cycle spend** | Credits used this billing cycle across SMTP, inbound, campaigns, verification and automations | | **Credits remaining** | Included credits left this cycle plus purchased credits | | **This billing period** | A daily chart of usage per product | | **Usage** | A table with usage for **SMTP / API**, **Inbound**, **Campaigns**, **Email verification** and **Automations** | ## When you run out Each action checks for credits before it runs. If the workspace doesn't have enough: | Action | What happens | | --- | --- | | API send, retry or forward | `402 Insufficient credits`. Nothing is sent or charged. See [Why do I get 402 Insufficient credits?](/docs/kb/402-insufficient-credits/) | | SMTP and campaign email | The email is accepted, then held with "Workspace has not enough email credits to send this email." | | Inbound email | The sending server gets `452 Insufficient credits to receive inbound email` and usually retries later. | | Email verification | `402`. Lists aren't started. | | Automation run | The run fails with the reason `insufficient_credits`. | After you buy credits, retry held emails from the email's detail page or with [Retry an email](/docs/api-reference/emails/retry/). Retrying creates a new email. See [Held emails](/docs/kb/email-status-held/). You get an email when your balance drops to 1,000 credits, unless you turned the alert off. See [Auto-refill and alerts](/docs/billing/auto-refill/). ## Refunds If you're not satisfied with your first credit purchase, and you've sent up to 1,000 emails, contact [support@emailit.com](mailto:support@emailit.com) within 30 days for a full refund. See the FAQ on the [pricing page](/pricing/). ## Related - [Plans](/docs/billing/plans/) - [Auto-refill and alerts](/docs/billing/auto-refill/) - [Invoices and billing details](/docs/billing/invoices/) --- Source: https://emailit.com/docs/billing/credits/ --- # Billing > How Emailit billing works, from plans and monthly credits to purchased credits, auto-refill and invoices, and who in a workspace can change it. Emailit bills each workspace separately, and everything you do is paid for in credits. A plan gives the workspace a monthly pool of included credits, and you buy more when you need them. This page explains the model and where to manage it. ## How it works 1. **A plan sets the basics.** Every workspace is on Pay as you go, Pro, Business or a Custom plan. The plan decides how many credits are included each month, what extra credits cost, and your limits. 2. **Actions use credits.** Each email, campaign email, automation run or verification uses a fixed number of credits. 3. **Included credits go first.** Emailit uses the month's included credits, then purchased credits. 4. **Buy more when you need them.** Purchased credits never expire. Turn on auto-refill so you don't run out. | | Pay as you go | Pro | Business | Custom | | --- | --- | --- | --- | --- | | Price | $0 | $25/mo, or $20/mo billed yearly | $250/mo, or $200/mo billed yearly | Custom | | Included credits / month | Up to 3,000 | 100,000 | 2,500,000 | Flexible | | Extra credits | $2 per 10,000 | $1 per 10,000 | $1 per 10,000 | Volume pricing | Each action costs: | Action | Credits | | --- | --- | | Email sent with the API or SMTP (per recipient) | 1 | | Inbound email received | 1 | | Campaign email (per recipient) | 2 | | Automation run | 3 | | Email verification (per address) | 5 | For current prices and the full comparison, see [pricing](/pricing/). ## Where billing lives Open **Workspace → Billing**. The page has three tabs: | Tab | What's there | | --- | --- | | **Workspace plan** | **Plans** (upgrade or switch), **Add-ons**, **Auto-refill**, the AppSumo license if you have one, **Payment method**, **Billing email**, **Billing address** and **Included bonus** | | **Billable usage** | Included and purchased credits, credits remaining, **Purchase**, a usage chart and a per-product table for the current billing period | | **Invoices** | Every invoice with date, number, amount, status and a PDF link | The sidebar also shows a **Credits** meter with how many credits are left. Admins see a **Top up** button there to buy credits. ## Who can manage billing Everyone in the workspace can see the billing page, usage and invoices. Only **Admins** can: - Buy credits or change the plan. - Activate add-ons. - Add, update or delete the payment method. - Change the billing email, invoice language, billing address and auto-refill settings. See [Members and roles](/docs/workspaces/members-and-roles/). ## Payments Emailit uses Stripe for payments. Plan upgrades, credit purchases and add-ons open a Stripe Checkout page, where you can also enter a promotion code. Auto-refill charges the default card saved on the workspace. All prices are in US dollars. Custom plans can pay by invoice and bank transfer. See [Plans](/docs/billing/plans/). ## Next steps - [Plans](/docs/billing/plans/): Compare plans, upgrade, switch to yearly or downgrade. - [Credits](/docs/billing/credits/): How credits are used, reset and bought. - [Auto-refill and alerts](/docs/billing/auto-refill/): Top up automatically and get warned before you run out. - [Invoices and billing details](/docs/billing/invoices/): Billing email, address, tax ID and payment methods. --- Source: https://emailit.com/docs/billing/ --- # Invoices and billing details > Find and download Emailit invoices, set the billing email and invoice language, add your billing address and tax ID, and manage the card on file. This page covers where to find invoices and how to set the details printed on them: billing email, invoice language, billing address and tax ID. It also shows how to manage the card Emailit charges. Each workspace has its own billing details. ## Before you begin - Anyone in the workspace can view invoices and billing details. - Only **Admins** can change them. See [Members and roles](/docs/workspaces/members-and-roles/). ## Download invoices Go to **Workspace → Billing** and select the **Invoices** tab. Invoices are issued for subscription renewals and credit purchases. The table lists the most recent invoices with: | Column | Details | | --- | --- | | **Date** | When the invoice was created | | **Type** | The document type | | **Number** | The invoice number | | **Amount** | Amount in US dollars | | **Status** | For example `paid` or `open` | Select **PDF** on a row to open or download the invoice. Use it as your receipt. ## Billing email The billing email is the address Stripe and Emailit use for this workspace's invoices. It starts as the email of the person who created the workspace. 1. **Open the card.** On the **Workspace plan** tab, find **Billing email** and select **Edit**. 2. **Enter the address.** In **Billing email preferences**, type the **New email** and the same address in **Confirm new email**. 3. **Choose PDF invoices.** Turn **Receive PDF invoices** on to get an email with a link whenever an invoice is paid. 4. **Pick a language.** Choose the **Invoice language** and select **Save**. Invoice languages: English, Čeština, Deutsch, Español, Français, Italiano, Nederlands, Polski, Português, 日本語, 中文, Українська and Русский. Invoice emails go to the **Notification emails** in the **Auto-refill** card if you've set any, and to the billing email otherwise. **Receive PDF invoices** is the same setting as **Email the same addresses when a new invoice is issued** in that card. See [Auto-refill and alerts](/docs/billing/auto-refill/). Changing the billing email doesn't change anyone's sign-in email. ## Billing address and tax ID The billing address is printed on your invoices and used for tax purposes. 1. **Open the dialog.** On the **Workspace plan** tab, find **Billing address** and select **Edit**. 2. **Fill in the fields.** **First and last name**, **Company**, **Address line 1**, **Address line 2 (optional)**, **City**, **State / region**, **Postal code**, **Country** and **Tax ID (optional)**, such as your EU VAT number. 3. **Save.** Select **Save**. New invoices use the updated details. Invoices are addressed to the **Company** and **First and last name** from the billing address. Until you save an address, they use the workspace name. Changing the billing address doesn't change the billing address of your card. ## Payment methods The **Payment method** card shows the workspace's **Primary** card: the last four digits, the cardholder name and the expiry date. Emailit charges it for auto-refills and subscription renewals. ### Add or update the card 1. **Open the dialog.** On **Payment method**, select **Add**, or **Update** if a card is already saved. 2. **Enter the card.** Fill in the Stripe card form in **Update payment method**: "Add or replace the card Emailit charges for subscriptions, credits, and auto-refill." 3. **Save.** Confirm. Your bank may ask you to approve the card. The new card becomes the default. ### Delete the card Select **Delete** on the **Payment method** card. Without a card, [auto-refill](/docs/billing/auto-refill/) can't charge, so add a new card first if you rely on it. Emailit accepts major credit and debit cards through Stripe. Custom plans can pay by invoice and bank transfer. See [Plans](/docs/billing/plans/#custom-plans). ## Related - [Billing](/docs/billing/) - [Credits](/docs/billing/credits/) - [Notification emails](/docs/account/notification-emails/) --- Source: https://emailit.com/docs/billing/invoices/ --- # Plans > Compare Emailit's Pay as you go, Pro, Business and Custom plans, switch to yearly billing, upgrade or downgrade a workspace and see what changes when you do. Each workspace is on one of four plans. The plan sets the monthly price, the included credits, the price of extra credits and most limits. This page compares them and shows how to change plans. ## Compare plans - **Pay as you go** costs nothing per month. You get the free monthly credits included with Pay as you go and buy extra credits when you need them. - **Pro** is for professional projects. It includes 100,000 credits a month, cheaper extra credits, advanced analytics and DMARC reports. - **Business** is for teams where email is critical. It includes 2,500,000 credits a month and adds queryable SQL analytics. - **Custom** (Contract on the pricing page) has flexible credits, limits, retention and invoicing, agreed with the Emailit team. | | Pay as you go | Pro | Business | Custom | | --- | --- | --- | --- | --- | | Price | $0 | $25/mo, or $20/mo billed yearly | $250/mo, or $200/mo billed yearly | Custom | | Included credits / month | Up to 3,000 | 100,000 | 2,500,000 | Flexible | | Extra credits | $2 per 10,000 | $1 per 10,000 | $1 per 10,000 | Volume pricing | | Sending domains | 3 (25 after your first credit purchase) | 100 | 1,000 | By agreement | | Webhook endpoints | 3 | 10 | 100 | 100 | | Webhook payload filters | — | Up to 25 rules | Up to 25 rules | Up to 25 rules | | Subscribers per audience | 10,000 | 50,000 | 250,000 | Unlimited | | Message contents kept | 7 days | 30 days | 30 days | Flexible | | Message activity kept | 30 days | 365 days | 365 days | Flexible | | Request logs kept | 7 days | 30 days | 30 days | Flexible | | Analytics kept | 365 days | Forever | Forever | Flexible | | Analytics | Dashboard | Dashboard + Advanced | Dashboard + Advanced + Queryable (SQL) | Dashboard + Advanced + Queryable (SQL) | | DMARC reports | — | Included | Included | Included | | Sending limit increases | On request | Automatic, based on sending health | Automatic, based on sending health | By agreement | | Review of domains under 30 days old | Yes | No | No | Yes | | Configurable automatic suppression | — | Included | Included | — | Every plan includes the Email API, SMTP relay, inbound email, templates, campaigns, automations and webhooks. See [pricing](/pricing/) for the full feature comparison. ## Monthly or yearly The **Plans** table has a **Yearly** switch. Yearly billing is 20% less: Pro is $20 a month billed yearly ($240 a year) and Business is $200 a month billed yearly ($2,400 a year). Included credits are still a monthly pool, whichever way you pay. ## Upgrade or switch plans 1. **Open Plans.** Go to **Workspace → Billing** and stay on the **Workspace plan** tab. You need the Admin role. 2. **Choose billing.** Turn on **Yearly** if you want annual billing. 3. **Pick a plan.** Select **Upgrade** next to Pro or Business, or **Switch** if you're moving to a lower paid plan. 4. **Pay in Stripe Checkout.** Enter your card details and confirm. You can add a promotion code here. When Stripe confirms the payment, you return to the dashboard and the plan shows **Active**, with **Renews** and the next renewal date. It can take a few seconds for the new plan to appear. ## What changes when you upgrade These take effect as soon as the new plan is active: - **Included credits** rise to the plan's monthly pool, and a partner or referral bonus doubles from 5,000 to 10,000 a month. - **Extra credits** cost $1 per 10,000 instead of $2. - **Limits** grow: sending domains, webhook endpoints and subscribers per audience. See [Limits and quotas](/docs/limits/). - **Analytics** unlocks the **Advanced** tab on Pro and Business, and the **Queryable** SQL tab on Business. See [Analytics](/docs/analytics/). - **DMARC reports** and **webhook payload filters** become available. - **Sending limits** start rising automatically every 7 days or more while sending health stays good. - **Domain review** for domains registered less than 30 days ago is skipped. See [Domain verification](/docs/domains/verification/). - **Automatic suppression** becomes configurable under **Workspace → Settings**. - **Retention** moves to the plan's defaults. Data that was already deleted under the old plan doesn't come back. ## Downgrade to Pay as you go Pay as you go has no subscription, so you return to it by canceling the paid subscription. Selecting **Downgrade** on the Pay as you go row shows: "Cancel the paid subscription from the current plan to return to Pay as you go." The dashboard doesn't have a cancel button, so email [support@emailit.com](mailto:support@emailit.com) from an admin account and name the workspace. When the subscription ends, the workspace moves to Pay as you go: - Plan limits, retention defaults and the extra-credit rate change to Pay as you go values. - Purchased credits stay in the workspace and never expire. - If the workspace has more sending domains or webhooks than Pay as you go allows, the existing ones keep working, but you can't add more until you're under the limit. The same happens if a subscription is canceled or ends unpaid. ## Custom plans Custom plans are for high volume, invoicing, extra retention or contract terms. On the **Plans** table, select **Contact sales** on the **Custom** row and fill in the form. The request appears under **Workspace → Settings → Requests**, where the team replies. See [Workspace requests](/docs/workspaces/requests/). You can also reach sales through the [contact form](/contact/). ## Related - [Credits](/docs/billing/credits/) - [Add-ons](/docs/billing/add-ons/) - [AppSumo licenses](/docs/billing/appsumo/) - [Pricing](/pricing/) --- Source: https://emailit.com/docs/billing/plans/ --- # Create a campaign > Walk through the five-step campaign wizard, from choosing audiences and the sender to content, testing and sending, and see exactly who receives it. This guide walks through the campaign wizard step by step: picking recipients, setting the sender and subject, writing the content, testing it and sending it. It also explains who ends up receiving the campaign and how to do the same with the API. ## Before you begin - Your workspace needs production access. Until then, campaigns can't be sent. See [Production access](/docs/workspaces/production-access/). - Verify the domain you'll send from. See [Add a domain](/docs/domains/add-a-domain/). - Create at least one [audience](/docs/audiences/) with subscribers. - Optional: prepare a [template](/docs/templates/) to start the content from. ## Create the campaign On **Email Marketing → Campaigns**, select **Add campaign**, enter a **Name** and select **Create**. The name is for you only. The wizard opens on its first step. The steps are listed on the left. A check mark shows which ones are complete, and you can go back to any step. **Continue** saves the current step and moves to the next. While the campaign is a draft, you can leave and come back to it from the Campaigns list. ## Step 1: Recipients Choose who the campaign goes to. - **Audiences:** pick one or more audiences. Each option shows how many subscribers it has. At least one is required. - **Exclude audiences:** optionally pick audiences whose subscribers should be left out. - **Estimated recipients:** updates as you change the selection. It counts the unique contacts subscribed to at least one selected audience, minus those subscribed to an excluded audience. Select **Continue** to save the recipients. ### Who actually receives the campaign Emailit works out the final list when sending starts, not when you pick the audiences. A contact receives the campaign when: 1. It's subscribed to at least one of the selected audiences. 2. Its marketing status is **Subscribed**. Contacts unsubscribed with the **Unsubscribe** bulk action or `unsubscribed: true` are skipped. 3. Its address isn't on the [suppression list](/docs/suppressions/), with any suppression type that hasn't expired. Each address receives one copy, even if it's in several selected audiences. People who join an audience after you schedule the campaign are included, and people who unsubscribe before sending starts are left out. The estimate doesn't subtract globally unsubscribed contacts or suppressed addresses, so the number of emails sent can be lower than the estimate. > **Excluded audiences aren't applied when sending:** Excluded audiences reduce the estimate, but the current version doesn't apply them when the campaign is sent. A contact who is in both a selected audience and an excluded audience still receives the campaign. Until this changes, don't rely on **Exclude audiences** to keep people out: remove them from the selected audiences, unsubscribe them, or send to an audience that doesn't include them. ## Step 2: Sender and Subject | Field | Required | Notes | | --- | --- | --- | | **From name** | Yes | The name recipients see, for example `Acme`. | | **From email** | Yes | An address on a domain verified in this workspace, for example `news@acme.com`. | | **Subject** | Yes | Can include [merge tags](/docs/campaigns/merge-tags/), for example `October news for {{first_name}}`. | | **Reply to** | No | Where replies go. Leave it empty to use the **From email**. | Select **Continue** to save. ## Step 3: Content Pick how you want to write the email: | Option | Best for | | --- | --- | | **Dragit editor** | Designing a layout with drag-and-drop blocks. Saves automatically as you work. | | **Rich-text editor** | Simple, text-first emails. Type `@` to insert a merge tag. | | **HTML editor** | Pasting or writing your own HTML. | | **Choose from templates** | Starting from one of your [templates](/docs/templates/). Emailit copies the template's content into the campaign, so later edits don't change the template. The template's subject isn't copied. | See [Editors](/docs/templates/editors/) for how each editor works. Once the campaign has content, the step shows a preview with these buttons: - **Open editor** reopens the editor you used. - **Remove content** clears the content so you can pick another editor. Switching editors always starts from empty content. - **Save as template** saves the content, subject and sender as a new template you can reuse in other campaigns or send with the API. Enter a **Name** and an **Alias**, then select **Save** or **Save & Open**. Add `{{unsubscribe_url}}` as a link in your content. In Dragit it's listed with the special links, and in the rich-text editor it's the **Unsubscribe URL** variable. See [Unsubscribes](/docs/audiences/unsubscribes/#the-link-in-your-content). Select **Continue** when the content is ready. ## Step 4: Preview and test - **Desktop** and **Mobile** show the email at each width. - **Preview as contact** fills in the merge tags with a real contact's name, email and custom fields. Search for a contact by name or email. - **Send test** sends the email to up to 5 addresses. See [Test and schedule](/docs/campaigns/test-and-schedule/) for details and limits. Select **Continue** when you're happy with it. ## Step 5: Send The last step summarizes the **From**, **Reply to**, **Subject**, **Recipients** and **Content**, with an **Edit** link back to each step, and runs these checks: | Check | Result | | --- | --- | | **From**, **Subject**, **Content** | Must pass. **Send campaign** stays disabled until all three are set. | | Unsubscribe link | "No problems detected!" when the HTML contains `{{unsubscribe_url}}`. Otherwise "Missing unsubscribe link!". This doesn't block sending, but fix it before you send. | | Size | "Your content will be clipped as it is bigger than 100kb." appears when the HTML is larger than about 102 KB. Gmail cuts off longer messages and hides the rest, including the footer and unsubscribe link, behind a link. Shorten the content or move images and styles out of the HTML. | Select **Send campaign**, then choose: - **Immediately**, then **Send now**, to start sending right away. - **Schedule**, then pick a date and time in **Schedule for**, in your computer's time zone, and select **Schedule**. The time must be in the future. The campaign page opens and shows the campaign as **In process** or **Scheduled**. See [Test and schedule](/docs/campaigns/test-and-schedule/) for what happens next. ## Verify it worked - The Campaigns list shows the campaign under **In process** or **Scheduled**, then **Sent**. - The campaign page shows a **Send progress** bar while emails are created, then the delivery and engagement counts. See [Campaign reports](/docs/campaigns/reports/). - Each recipient email appears under **Email API → Emails** with its own status. ## Create a campaign with the API The same steps with the [Campaigns API](/docs/api-reference/campaigns/), using an API key with **Full Access**: 1. **Create the draft** with the sender, subject and HTML. ```bash curl https://api.emailit.com/v2/campaigns \ -X POST \ -H "Authorization: Bearer $EMAILIT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "October newsletter", "from_name": "Acme", "from_email": "news@acme.com", "reply_to": "support@acme.com", "subject": "October news for {{first_name}}", "html": "

Hi {{first_name}},

Here is what is new.

Unsubscribe

", "text": "Hi {{first_name}}, here is what is new. Unsubscribe: {{unsubscribe_url}}" }' ``` The response contains the campaign `id`, for example `cmp_4Tq9Xv2kLm8Rw`, with `status` `draft`. 2. **Set the recipients** with [Update a campaign](/docs/api-reference/campaigns/update/). `recipients` replaces the current list and needs at least one audience. ```bash curl https://api.emailit.com/v2/campaigns/cmp_4Tq9Xv2kLm8Rw \ -X POST \ -H "Authorization: Bearer $EMAILIT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "recipients": [{ "audience_id": "aud_5hJ2kL8mNp4Qr" }] }' ``` 3. **Send it** with [Send or schedule a campaign](/docs/api-reference/campaigns/send/), or schedule it with `scheduled_at`. See [Test and schedule](/docs/campaigns/test-and-schedule/#use-the-api). ```bash curl https://api.emailit.com/v2/campaigns/cmp_4Tq9Xv2kLm8Rw/send \ -X POST \ -H "Authorization: Bearer $EMAILIT_API_KEY" ``` ## Related - [Merge tags](/docs/campaigns/merge-tags/): Personalize the subject and content. - [Test and schedule](/docs/campaigns/test-and-schedule/): Test sends, scheduling and canceling. --- Source: https://emailit.com/docs/campaigns/create/ --- # Campaigns > Campaigns send one email to the subscribers of your audiences. Learn the requirements, statuses, credit cost and what the Campaigns list and API can do. A campaign sends one email, such as a newsletter or a product announcement, to everyone subscribed to the audiences you choose. You build it in a five-step wizard in the dashboard, test it, then send it right away or schedule it, and follow the results in its report. ## Before you send | Requirement | Why | | --- | --- | | **Production access** | New workspaces start in sandbox mode, where campaigns can't be sent. Request access under **Workspace → Settings → Requests**. See [Production access](/docs/workspaces/production-access/). | | **A verified sending domain** | The **From email** must use a domain you've verified in this workspace. See [Add a domain](/docs/domains/add-a-domain/). | | **An audience with subscribers** | Campaigns send to [audiences](/docs/audiences/). Add people by hand, [import them](/docs/contacts/import-export/) or collect them with a [subscribe URL](/docs/audiences/subscribe-url/). | | **An unsubscribe link** | Put `{{unsubscribe_url}}` in your content. See [Unsubscribes](/docs/audiences/unsubscribes/). | | **Enough credits** | Each campaign email costs 2 credits. | For loads and clicks to show up in the report, the sending domain also needs a verified tracking subdomain. Campaign emails are tracked automatically when it's in place. See [Tracking](/docs/tracking/). ## How it works 1. **Create the campaign.** On **Email Marketing → Campaigns**, select **Add campaign** and give it a name. Recipients never see the name. 2. **Work through the wizard:** **Recipients**, **Sender and Subject**, **Content**, **Preview and test** and **Send**. See [Create a campaign](/docs/campaigns/create/). 3. **Send or schedule it.** Emailit works out the recipients when sending starts and creates one email per recipient. See [Test and schedule](/docs/campaigns/test-and-schedule/). 4. **Read the report.** Deliveries, loads, clicks and unsubscribes appear on the campaign page as they happen. See [Campaign reports](/docs/campaigns/reports/). ## Credits A campaign creates one email per recipient, and each one costs 2 credits. A campaign to 10,000 subscribers uses 20,000 credits. Credits come from your plan's monthly included credits first, then from purchased credits. See [Credits](/docs/billing/credits/) and [pricing](/pricing/). ## Campaign statuses | Status | API value | Meaning | | --- | --- | --- | | **Draft** | `draft` | Being edited. Only drafts can be sent, scheduled or deleted. | | **Scheduled** | `scheduled` | Waiting for its send time. You can cancel the schedule up to 5 minutes before. | | **In process** | `queued`, `sending` | Sending has started and Emailit is creating the recipient emails. | | **Sent** | `sent` | Every recipient email has been created and handed to delivery. Deliveries, loads and clicks keep updating afterwards. | | **Canceled** | `canceled` | The campaign was canceled with the API before it finished sending. | | **Archived** | `archived` | A sent campaign you've moved out of the way. Its report stays available. | Status changes produce `campaign.*` events, such as `campaign.scheduled`, `campaign.sending` and `campaign.sent`, that you can receive with [webhooks](/docs/webhooks/event-types/). ## The Campaigns list **Email Marketing → Campaigns** lists your campaigns with tabs for **All**, **Draft**, **Scheduled**, **In process**, **Sent** and **Archived**. Canceled campaigns appear under **All**. Columns show the **Name**, **Status**, **Last updated**, **Created** and **Sent** times, and **Stats** with the sent, clicked and unsubscribed counts. Search matches the name and subject. Selecting a draft opens the wizard. Selecting any other campaign opens its report. The row menu offers these actions: | Action | Available for | What it does | | --- | --- | --- | | **Rename** | All campaigns | Changes the internal name. | | **Duplicate** | All campaigns | Creates a new draft with the same sender, subject and content. The name defaults to the original name plus " (Copy)". Recipients aren't copied, so pick audiences again. | | **Save as template** | Campaigns with content | Saves the content, subject and sender as a new [template](/docs/templates/). | | **Cancel Schedule** | Scheduled | Moves the campaign back to **Draft**. Not possible in the last 5 minutes before the send time. | | **Archive** | Sent | Moves the campaign to the **Archived** tab. | | **Unarchive** | Archived | Moves the campaign back to **Draft**. | | **Delete** | Drafts | Deletes the campaign. This can't be undone. | ## Use the API The [Campaigns API](/docs/api-reference/campaigns/) covers the same lifecycle. It needs an API key with **Full Access**. | Endpoint | Use it to | | --- | --- | | [Create a campaign](/docs/api-reference/campaigns/create/) | Create a draft with `name`, `subject`, `from_email`, `from_name`, `reply_to`, `html` and `text`. | | [Update a campaign](/docs/api-reference/campaigns/update/) | Change the content or set `recipients`, a list of `{ "audience_id": "aud_…" }` objects. | | [Send or schedule a campaign](/docs/api-reference/campaigns/send/) | Send now, or pass a future `scheduled_at` to schedule a draft. | | [Cancel a campaign](/docs/api-reference/campaigns/cancel/) | Cancel a draft or a campaign that's still sending. | | [List](/docs/api-reference/campaigns/list/), [retrieve](/docs/api-reference/campaigns/get/) and [delete](/docs/api-reference/campaigns/delete/) | Read and clean up campaigns. Filter the list by `status`. | You can use a campaign's name instead of its `cmp_` ID in the URL. Sending from a workspace without production access returns `403`. Campaign statistics are available in the dashboard only. ## Next steps - [Create a campaign](/docs/campaigns/create/): Walk through the five-step wizard. - [Merge tags](/docs/campaigns/merge-tags/): Personalize subjects and content. - [Test and schedule](/docs/campaigns/test-and-schedule/): Send tests, then send now or later. - [Campaign reports](/docs/campaigns/reports/): Every metric, tab and export. --- Source: https://emailit.com/docs/campaigns/ --- # Merge tags > Personalize campaign subjects and content with merge tags for names, email, custom fields and the unsubscribe link, and see how they differ from Temple. Merge tags insert each recipient's own data into a campaign: their name in the subject, a custom field in the body, and their personal unsubscribe link. This page lists every tag, the syntax rules and how to preview the result. ## Available tags | Tag | Replaced with | | --- | --- | | `{{first_name}}` | The contact's first name. | | `{{last_name}}` | The contact's last name. | | `{{email}}` | The contact's email address. | | `{{unsubscribe_url}}` | The recipient's personal unsubscribe link. See [Unsubscribes](/docs/audiences/unsubscribes/). | | `{{cf.}}` | The value of a [custom field](/docs/contacts/custom-fields/), by its key. For example, `{{cf.company}}` for a field with the key `company`. | Merge tags work in the campaign **Subject**, the HTML content and the plain-text version. They aren't replaced in the **From name**, **From email** or **Reply to**. ```html

Hi {{first_name}},

Your {{cf.plan}} plan now includes scheduled sending.

Unsubscribe from Acme emails sent to {{email}}.

``` ## Insert tags in the editors - **Rich-text editor:** type `@` and choose a variable, or a custom field by its name. - **Dragit editor:** use the merge fields menu, which lists First name, Last name, Email and your custom fields. The unsubscribe link is under the special links. - **HTML editor** and the **Subject** field: type the tag. ## Syntax rules - **No spaces inside the braces.** Write `{{first_name}}`. A tag with spaces, such as `{{ first_name }}`, isn't replaced and appears in the email as typed. This matters most for the unsubscribe link: the **Send** step's check accepts `{{ unsubscribe_url }}`, but only `{{unsubscribe_url}}` becomes a working link. - **Tag names are not case-sensitive**, so `{{First_Name}}` works too. Custom field keys must match the stored key exactly, and keys are always lowercase, so write `{{cf.company}}`, not `{{cf.Company}}`. - **Custom fields need the `cf.` prefix.** `{{company}}` isn't a merge tag and appears as typed. - **Anything else in double braces stays as typed.** Emailit only replaces the tags in the table above. ## Missing values When a contact has no value for a tag, the tag is replaced with nothing. There's no fallback syntax in campaigns: `{{first_name|"there"}}` isn't supported and appears as typed. | Content | Contact with a first name | Contact without one | | --- | --- | --- | | `Hi {{first_name}},` | Hi Ada, | Hi , | | `Hi there,` | Hi there, | Hi there, | So either write copy that still reads well without the value, or make sure everyone has one. On the Contacts list, filter by **First name** with **is empty** to find contacts with a missing name. ## How values are rendered | Field type | Rendered as | | --- | --- | | Text, Number | The stored value, for example `Acme` or `42`. | | Date | `YYYY-MM-DD`, for example `1990-04-12`. | | Boolean | `true` or `false`. | | Select | The selected option. | | Multi select | The options separated by commas, without spaces, for example `news,offers`. | Values are inserted exactly as stored, without HTML escaping. If people can enter their own names, for example through a public sign-up form, check those values before you use them in HTML. ## Preview merge tags - **Preview as contact**, in the **Preview and test** step, renders the subject and content with a real contact's data. Use it to check names, custom fields and empty values. - **Test sends** fill `{{email}}` with the test address and leave names and custom fields empty. The unsubscribe link in a test doesn't unsubscribe anyone. See [Test and schedule](/docs/campaigns/test-and-schedule/). ## Merge tags and Temple Campaigns use their own merge tags. Templates sent through the [Email API](/docs/email-api/) and emails sent by [automations](/docs/automations/) use [Temple](/docs/templates/temple/), Emailit's template language, which looks similar but works differently: | | Campaign merge tags | Temple | | --- | --- | --- | | Used in | Campaigns | API and SMTP sends with a template, automation emails | | Data | The recipient contact | The `variables` you pass, or the automation run's contact and event data | | Fixed set of tags | Yes, the five tags above | No, any variable you pass | | Nested values | Only `cf.` | Any path, such as `{{user.name}}` | | Fallback values | No | Yes | | Conditionals | No | Yes, `{{#if ...}}` | | Unsubscribe link | `{{unsubscribe_url}}` | Not provided | In Temple, a fallback goes after a pipe, for example `{{first_name|"there"}}`. If you save a campaign as a template and send it with the API, Temple fills its tags from the `variables` you pass. Pass `first_name` for `{{first_name}}`, a `cf` object for `{{cf.company}}` and your own `unsubscribe_url` if the content links to one. In automations, contact custom fields are available as `{{custom_fields.}}`, not `{{cf.}}`. ## Related - [Custom fields](/docs/contacts/custom-fields/): Define the fields you use in merge tags. - [Temple](/docs/templates/temple/): Variables, fallbacks and conditionals for API sends. --- Source: https://emailit.com/docs/campaigns/merge-tags/ --- # Campaign reports > Read a campaign report: every delivery and engagement metric, results by mailbox provider, who loaded and clicked, unsubscribes and the PDF export. Every campaign that has been scheduled or sent has a report. It shows how many emails were delivered, loaded and clicked, who engaged, who unsubscribed and how each mailbox provider handled the campaign. This page defines every metric and section, and covers the PDF export and the events you can use to build your own reporting. ## Open a report On **Email Marketing → Campaigns**, select any campaign that isn't a draft. The report has four tabs, **Overview**, **Loads**, **Clicks** and **Unsubscribes**, and these actions at the top: - **Export report** downloads a PDF. See [Export a PDF report](#export-a-pdf-report). - **Save as template** saves the content as a new [template](/docs/templates/). - **Cancel Schedule** for scheduled campaigns. See [Test and schedule](/docs/campaigns/test-and-schedule/#cancel-a-schedule). - **Archive** for sent campaigns, or **Unarchive** for archived ones. The metric cards stay visible on every tab. Counts update as deliveries, loads and clicks come in. ## Metrics While the campaign is **In process**, the cards show **Send progress** with **Sent**, **Delivered**, **Attempted**, **Not processed**, **Bounced**, **Suppressed** and **Complained**. Once it's sent, they show **Sent**, **Loaded**, **Clicked**, **Click-through rate**, **Unsubscribed**, **Bounced**, **Suppressed** and **Complained**. Each card shows a count and, except for the click-through rate, a percentage of **Sent**. | Metric | Definition | | --- | --- | | **Sent** | Emails created for the campaign, one per recipient, whatever their status. Addresses skipped before sending, because they were unsubscribed or suppressed, aren't included. | | **Delivered** | Emails accepted by the recipient's mail server. Includes emails that were later loaded or clicked. | | **Attempted** | Emails whose last delivery attempt failed temporarily. Emailit keeps retrying them for up to about 21 hours before they bounce. | | **Not processed** | Emails created but not yet handed to delivery. Only non-zero while the campaign is sending. | | **Bounced** | Emails that failed permanently, for example because the mailbox doesn't exist. | | **Suppressed** | Emails not delivered because the address was on your [suppression list](/docs/suppressions/) when Emailit tried to deliver it. | | **Complained** | Emails the recipient reported as spam, as reported back by their mailbox provider. | | **Loaded** | Emails opened at least once, counted once per email (unique opens). | | **Clicked** | Emails with at least one tracked link clicked, counted once per email. | | **Click-through rate** | **Clicked** as a percentage of **Loaded**: the share of opened emails that got a click. | | **Unsubscribed** | Recipients who unsubscribed with this campaign's unsubscribe link. | Each email has one status at a time, and it only moves forward, from delivered to loaded to clicked. A bounced, suppressed or complained email keeps that status. See [Email statuses](/docs/logs/email-statuses/). ### Loads and clicks need tracking **Loaded** and **Clicked** come from tracking pixels and tracked links on your sending domain's tracking subdomain. Campaign emails are tracked automatically when the domain has a verified tracking subdomain. Without one, emails go out untracked and both metrics stay at 0. See [Custom tracking domain](/docs/tracking/custom-tracking-domain/). Treat loads as an estimate: some email apps block images, so real opens are missed, and some privacy features load images automatically, which records opens that didn't happen. ## Overview tab ### Delivery by provider This table splits the campaign by the recipient's mailbox provider, based on the domain of each address: | Provider | Addresses | | --- | --- | | **Gmail** | `gmail.com`, `googlemail.com` | | **Outlook** | `outlook.com` | | **Live** | `live.com` | | **Hotmail** | `hotmail.com` | | **Yahoo** | `yahoo.com`, other `yahoo.` domains, `ymail.com`, `rocketmail.com` | | **iCloud** | `icloud.com`, `me.com`, `mac.com` | | **Other** | Every other domain, including company domains and Google Workspace or Microsoft 365 mailboxes on custom domains | For each provider it shows **Sent** (with its share of the campaign), **Delivered**, **Loaded**, **Clicked** and **Bounced** (each as a share of that provider's sent emails). Providers without sends aren't listed. A provider with a much lower delivered rate or higher bounce rate than the rest points to a deliverability problem with that provider. See [Deliverability best practices](/docs/deliverability/best-practices/). ### Content and details The **Content** card shows the campaign as sent, with tabs for **HTML**, **Text** and **Source**. Next to it are the **Subject**, the **Status** and the time the campaign was **Sent** (**Started** while it's sending, or **Scheduled** for a scheduled campaign). ## Loads tab The **Loads** tab shows **Unique loads** and **Total opens**, and lists each recipient who loaded the email with **Email**, **First name**, **Last name**, **Total opens**, **IP address**, **First Opened** and **Last Opened**. Search by email address, or filter by email and date. ## Clicks tab The **Clicks** tab shows **Total Links**, **Unique Clickers** and **Total Clicks**, and lists every tracked link with its **URL**, **Unique clicks**, **Total clicks**, **First Click** and **Last Click**. Select **View Clickers** on a link to see who clicked it: **Email**, **Total clicks**, **IP address**, **First click** and **Last click** for each recipient. ## Unsubscribes tab The **Unsubscribes** tab lists recipients who unsubscribed with this campaign's link, with **Email**, **First name**, **Last name**, **Reason**, **IP address** and **Unsubscribed At**. Search it, or filter by reason or date. The reason, when one was recorded, shows as **Manual**, **Unsubscribe Link** or **Complaint**. Unsubscribes you make yourself in the dashboard or API aren't linked to a campaign and don't appear here. See [Unsubscribes](/docs/audiences/unsubscribes/). ## Export a PDF report 1. **Open the export.** On the campaign page, select **Export report**. 2. **Set the filters.** Under **Filters**, pick a **Date range** (**All time**, **Last 7 days**, **Last 30 days** or **Last 90 days**) and optionally a **Status**: delivered, loaded, clicked, bounced, failed, rejected, suppressed or complained. The preview on the right updates as you change them. 3. **Download.** Select **Generate report**. Your browser downloads the PDF. The report includes: - The campaign name, subject, status, the date range and status filter you chose, and when the report was generated. - **Overview** counts: sent, delivered, loaded, clicked, bounced and unsubscribed. - **Result rates**: **Delivery rate**, **Load rate**, **Click rate**, **Bounce rate** and **Unsubscribe rate**, each as a share of sent emails. - An **Engagement funnel** from sent to delivered, loaded and clicked. - **Loads**, **Clicks** and **Top links**. - **Unsubscribe reasons**. The filters apply to the overview counts and rates, based on when each email was created. Load, click and unsubscribe details always cover the whole campaign. ## Events and webhooks Campaign statistics aren't available through the API. To build your own reporting, use events: - **Campaign status:** `campaign.created`, `campaign.updated`, `campaign.scheduled`, `campaign.queued`, `campaign.sending`, `campaign.sent`, `campaign.canceled`, `campaign.archived` and `campaign.deleted`. Their payload includes the campaign's `id`, `name`, `status`, `subject`, `from_email`, `from_name`, `scheduled_at`, `sent_at` and, for changes, `previous_status`. - **Per recipient:** every campaign email produces the usual email events, such as [`email.delivered`](/docs/webhooks/events/email/delivered/), [`email.bounced`](/docs/webhooks/events/email/bounced/), [`email.loaded`](/docs/webhooks/events/email/loaded/) and [`email.clicked`](/docs/webhooks/events/email/clicked/). Load and click events include the campaign in `email.campaign`, so you can group them by campaign. - **Unsubscribes:** `email.unsubscribed` and `email.resubscribed` when recipients use the unsubscribe link. Receive them with [webhooks](/docs/webhooks/), or read them from the [Events API](/docs/api-reference/events/). See [Event types](/docs/webhooks/event-types/) for every event. On the Business plan, [Queryable analytics](/docs/analytics/queryable/) can also group sends, bounces, loads and clicks by campaign. ## Related - [Bounces and complaints](/docs/deliverability/bounces-and-complaints/): Why emails bounce and how complaints are handled. - [Sending health](/docs/deliverability/sending-health/): How bounce rates affect your workspace. --- Source: https://emailit.com/docs/campaigns/reports/ --- # Test and schedule a campaign > Send test emails, preview a campaign as a real contact, send it now or schedule it, cancel a schedule, and follow what happens while a campaign is sending. Before a campaign goes out, check it in your own inbox and against real contact data. Then send it right away or pick a time. This page covers testing, scheduling, canceling a schedule and what happens while the campaign is sending, in the dashboard and with the API. ## Send a test Test sends go to your own addresses so you can check the email in real inboxes and email clients. 1. **Open the test step.** In the campaign wizard, go to **Preview and test**. 2. **Enter addresses.** Under **Send test**, type up to 5 addresses, separated by commas, spaces or semicolons, for example `me@acme.com, colleague@acme.com`. 3. **Send.** Select **Send test**. The confirmation reads "Test email is being sent." Test sends differ from the real campaign: - **Merge tags use placeholder data.** `{{email}}` becomes the test address. First name, last name and custom fields are empty. To check real values, use [Preview as contact](#preview-as-a-contact). - **The unsubscribe link doesn't unsubscribe anyone.** - **Loads and clicks aren't tracked**, and tests don't count in the campaign's report. - **Limits:** up to 5 addresses per test and 3 tests per minute. The campaign needs a **From email** first. - **Sandbox workspaces** can only send tests to the account addresses of workspace members. See [Production access](/docs/workspaces/production-access/). Test emails appear under **Email API → Emails** like other outgoing mail. ## Preview as a contact In the same step, **Preview as contact** renders the campaign with one contact's data. Search for a contact by name or email, and the preview shows the subject and content as that person will see them, with "Previewing as" and their name above it. Switch between **Desktop** and **Mobile** to check both widths. Use it to catch empty names, wrong custom field keys and tags that weren't replaced. See [Merge tags](/docs/campaigns/merge-tags/). ## Send now or schedule In the **Send** step, select **Send campaign** and choose when: | Option | What happens | | --- | --- | | **Immediately** | Select **Send now**. The campaign switches to **In process** and sending starts right away. | | **Schedule** | Pick a date and time under **Schedule for** and select **Schedule**. The campaign switches to **Scheduled**. | When you schedule: - **The time is in your computer's time zone.** The field defaults to the next full hour. Emailit stores the time in UTC. - **It must be in the future.** Otherwise the dialog shows "Pick a time in the future." - **Sending starts within a minute of the time.** Emailit checks for due campaigns every minute. - **Recipients are worked out when sending starts**, so people who join or leave your audiences in the meantime are counted correctly. - **The campaign is locked.** Only drafts can be edited. To change a scheduled campaign, cancel the schedule first. ## Cancel a schedule 1. **Find the campaign.** On **Email Marketing → Campaigns**, open the **Scheduled** tab. 2. **Cancel.** In the row menu, or at the top of the campaign page, select **Cancel Schedule** and confirm. The campaign goes back to **Draft** without a send time, so you can edit it and send or schedule it again. You can't cancel in the last 5 minutes before the scheduled time. The dashboard then shows "Campaign cannot be canceled less than 5 minutes before scheduled time." ## While the campaign is sending When sending starts, Emailit selects the recipients, skips unsubscribed and suppressed addresses, and creates one email per recipient in batches. Meanwhile the campaign page shows: - An **In process** card: "This campaign is still sending. Counts update as messages move through delivery." - **Send progress**: the share of created emails that have left the queue, for example "1,200 of 5,000 created messages have left the unprocessed queue." - Live counts for **Sent**, **Delivered**, **Attempted**, **Not processed**, **Bounced**, **Suppressed** and **Complained**. See [Campaign reports](/docs/campaigns/reports/#metrics). Once every recipient email is created, the status changes to **Sent**. Delivery carries on after that: emails to servers that are temporarily unavailable are retried for up to about 21 hours, and loads and clicks keep arriving. The report updates as they do. A campaign that has started sending can't be stopped from the dashboard, so test it before you send. ## Use the API Send a draft right away with [Send or schedule a campaign](/docs/api-reference/campaigns/send/): ```bash curl https://api.emailit.com/v2/campaigns/cmp_4Tq9Xv2kLm8Rw/send \ -X POST \ -H "Authorization: Bearer $EMAILIT_API_KEY" ``` ```json { "object": "campaign", "id": "cmp_4Tq9Xv2kLm8Rw", "name": "October newsletter", "status": "sending", "message": "Campaign send initiated" } ``` To schedule it, pass `scheduled_at` as an ISO 8601 date-time in the future. A Unix timestamp works too. Only drafts can be scheduled: ```bash curl https://api.emailit.com/v2/campaigns/cmp_4Tq9Xv2kLm8Rw/send \ -X POST \ -H "Authorization: Bearer $EMAILIT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "scheduled_at": "2026-10-15T09:00:00Z" }' ``` ```json { "object": "campaign", "id": "cmp_4Tq9Xv2kLm8Rw", "name": "October newsletter", "status": "scheduled", "scheduled_at": "2026-10-15T09:00:00Z", "message": "Campaign is scheduled" } ``` | Error | Cause | | --- | --- | | `403` | The workspace doesn't have production access yet. | | `422` "Invalid scheduled_at" | `scheduled_at` isn't a valid date in the future. | | `422` "Campaign cannot be scheduled" | The campaign isn't a draft. | [Cancel a campaign](/docs/api-reference/campaigns/cancel/) sets a `draft` or `sending` campaign to `canceled`. Emails that were already created for recipients still go out. To move a scheduled campaign back to draft, use **Cancel Schedule** in the dashboard. Follow progress with the `campaign.scheduled`, `campaign.queued`, `campaign.sending`, `campaign.sent` and `campaign.canceled` [webhook events](/docs/webhooks/event-types/). ## Related - [Campaign reports](/docs/campaigns/reports/): What each metric means. - [Create a campaign](/docs/campaigns/create/): The full wizard. --- Source: https://emailit.com/docs/campaigns/test-and-schedule/ --- # Custom fields > Define custom fields for your contacts, set their values from the dashboard, imports and the API, and use them in filters, campaign merge tags and automations. Custom fields store extra data on each contact, such as a company name, a plan or a birthday. You define the fields once for the workspace, then fill them in on contacts and use them to filter contacts, personalize campaigns and start automations. ## Field types | Type | Stored value | Example | Dashboard input | | --- | --- | --- | --- | | **Text** | String | `"Acme"` | Text box | | **Number** | Number | `42` | Number box | | **Date** | Calendar date, `YYYY-MM-DD` | `"1990-04-12"` | Date picker | | **Boolean** | `true` or `false` | `true` | Checkbox | | **Select** | One option | `"pro"` | Dropdown | | **Multi select** | Array of options | `["news", "offers"]` | Multi-select dropdown | Each field has a **name**, which the dashboard shows, and a **key**, which the API, imports, filters and merge tags use. Values are stored on the contact as a JSON object keyed by field key: ```json { "company": "Acme", "plan": "pro", "birthday": "1990-04-12", "interests": ["news", "offers"] } ``` ## Create a custom field 1. **Open Custom fields.** Go to **Workspace → Settings → Custom fields** and select **Add custom field**. 2. **Name the field.** Enter a **Name**, for example `Company size`. Emailit builds the key from the name automatically: lowercase, with every run of other characters replaced by `_`, so `Company size` becomes `company_size`. To choose the key yourself, select **Show advanced options** and edit **Key**. Keys are always saved in that lowercase, underscore form, and each key can exist only once per workspace. 3. **Pick the type.** Choose **Text**, **Number**, **Date**, **Boolean**, **Select** or **Multi select**. 4. **Add options for select fields.** For **Select** and **Multi select**, enter at least one option and use **Add option** for more. These values appear in the dropdown on contacts. 5. **Save.** Select **Create**. The field appears on every contact, in the contact filters and as a merge tag in the campaign editors. The Custom fields page lists every field with its name, type and options. Use **Edit** to rename a field, change its type or key, or edit its options. > **Deleting a field:** Deleting a custom field removes it from the dashboard, filters, imports and merge tags, and you can't undo it. Before you change or delete a key, update any campaigns, automations and API code that use it. ## Set values | Where | How | | --- | --- | | Dashboard | **Add contact** or **Edit** on a contact. Select **Show custom fields** to see the inputs. | | Import | Map a file column to the custom field in the import wizard. See [Import and export contacts](/docs/contacts/import-export/). | | API | Send a `custom_fields` object keyed by field key. | With the API, pass `custom_fields` to [Create a contact](/docs/api-reference/contacts/create/) or [Update a contact](/docs/api-reference/contacts/update/): ```bash curl https://api.emailit.com/v2/contacts/ada@example.com \ -X POST \ -H "Authorization: Bearer $EMAILIT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "custom_fields": { "company": "Acme", "plan": "pro", "birthday": "1990-04-12", "interests": ["news", "offers"] } }' ``` Rules to know: - **`custom_fields` replaces the whole object.** On update, include every value you want to keep, not only the ones that change. - **Use field keys, not names.** Values for keys that aren't defined in **Custom fields** are stored but don't show in the dashboard. - **Dates must be `YYYY-MM-DD`.** Emailit converts ISO date-times and Excel date cells to a date. Anything else returns `400` with "Custom field "Birthday" must be a date in YYYY-MM-DD format". Dates have no time or time zone. - **Select values aren't checked against the options.** A value that isn't in the option list is still stored, and the dashboard keeps showing it. ## Filter contacts by a custom field In the dashboard, open **Email Marketing → Contacts**, select **Filter** and pick the custom field by its name. Custom field filters compare the stored value as text, so use **equals**, **does not equal**, **contains**, **does not contain**, **starts with**, **ends with**, **is empty** or **is not empty**. With the API, use `custom_fields..` on [List contacts](/docs/api-reference/contacts/list/) and [Export contacts](/docs/api-reference/contacts/export/), with the same text conditions (`exact`, `not_exact`, `contains`, `not_contains`, `starts_with`, `ends_with`, `empty`, `not_empty`): ```bash curl "https://api.emailit.com/v2/contacts?custom_fields.plan.exact=pro&custom_fields.company.contains=acme" \ -H "Authorization: Bearer $EMAILIT_API_KEY" ``` For a range, such as birthdays in the 1990s, use the `filter[custom_fields.][gte]` and `[lte]` parameters. They compare the stored text, which sorts correctly for `YYYY-MM-DD` dates: ```bash curl -G "https://api.emailit.com/v2/contacts" \ --data-urlencode "filter[custom_fields.birthday][gte]=1990-01-01" \ --data-urlencode "filter[custom_fields.birthday][lte]=1999-12-31" \ -H "Authorization: Bearer $EMAILIT_API_KEY" ``` See [Filtering](/docs/api-reference/filtering/) for how filters combine. ## Use custom fields in campaigns In campaign content and subjects, insert a custom field with the merge tag `{{cf.}}`, for example `{{cf.company}}`. The rich-text and Dragit editors list your custom fields under their variables, so you don't have to type the key. ```html

Hi {{first_name}}, here's what's new for {{cf.company}}.

``` If a contact has no value, the tag is replaced with nothing. Multi select values render as a comma-separated list. See [Merge tags](/docs/campaigns/merge-tags/). ## Use custom fields in automations - **Date anniversary trigger.** Pick a **Date** field to start a run every year on the month and day stored in it, for example a birthday. See [Triggers](/docs/automations/triggers/#date-anniversary). - **Contact updated trigger.** Filter on a custom field, or on its previous value, to react when it changes. - **Condition step.** Branch on a custom field value. - **Edit contact step.** Set a custom field by entering its key. ## API reference Custom field definitions are managed in the dashboard only. Contact values use the `custom_fields` object on the [Contacts API](/docs/api-reference/contacts/), and the same object is accepted when you [add a subscriber](/docs/api-reference/audiences/subscribers/add/) to an audience. ## Related - [Contacts](/docs/contacts/): How contacts, audiences and subscribers fit together. - [Merge tags](/docs/campaigns/merge-tags/): Personalize campaigns with contact data. --- Source: https://emailit.com/docs/contacts/custom-fields/ --- # Import and export contacts > Import contacts from a CSV or Excel file with the import wizard, export filtered contacts to CSV or XLSX, and run bulk actions and exports with the API. Use the import wizard to bring contacts in from a spreadsheet, and export to download the contacts that match your current filters. This page also covers the API's bulk actions and export endpoint for doing the same from code. ## Before you begin - Create any [custom fields](/docs/contacts/custom-fields/) you want to fill from the file. The wizard can only map columns to fields that already exist. - Create the [audiences](/docs/audiences/) you want the contacts to join. Imports count toward the subscriber limit of each audience. - Only import people who agreed to hear from you. Production access reviews ask how you collect subscribers. See [Production access](/docs/workspaces/production-access/). ## Prepare your file The wizard reads `.csv`, `.xlsx` and `.xls` files with up to **2,500 contacts per file**. Split larger lists into several files. ```csv title="contacts.csv" email,first_name,last_name,company,birthday ada@example.com,Ada,Lovelace,Acme,1990-04-12 grace@example.com,Grace,Hopper,Acme,1906-12-09 alan@example.com,Alan,Turing,, ``` Tips for a clean import: - **Put one contact on each row** and the column names in the first row. Excel files are read from the first sheet only. - **Name columns after your fields** so the wizard maps them for you. A column header maps automatically when it matches a field's name or key, ignoring case, for example `email`, `First name`, `first_name` or a custom field key such as `company`. - **Save CSV files as UTF-8** so names with accents come through correctly. - **Write dates as `YYYY-MM-DD`.** Date cells in Excel files work too. Other date formats are rejected. - **Check the addresses.** One invalid email address stops the whole import with an error that names the row, for example "Contact #12 has an invalid email address." Rows with an empty email cell are skipped. - **Multi select values** import as a single text value. To store several options as a list, set them with the API. ## Import contacts 1. **Open the wizard.** Go to **Email Marketing → Contacts** and select **Import**. 2. **Choose the file.** Under **CSV File**, select your file. Leave **File has header** checked if the first row holds column names. The wizard shows the first rows and the total row count, with a warning if the file has more than 2,500 rows. Select **Continue**. 3. **Map the columns.** For each column, pick the contact field it fills: **Email**, **First name**, **Last name**, one of your custom fields, or **Exclude** to skip it. You must map one column to **Email**. Each row shows sample values so you can check the mapping. 4. **Pick audiences.** Under **Audiences**, choose the audiences the contacts should join, or leave it empty to import contacts without adding them to a list. Select **Continue**. 5. **Review and import.** The preview shows the file, the contact count, the audiences, the column mapping and the first 10 contacts as they'll be saved. Select **Import**. Emailit validates the whole file first. If anything is wrong, such as an invalid address, an unknown custom field or an audience that's full, nothing is imported and the errors are listed so you can fix the file. Otherwise the import runs in the background in batches of 500. Refresh the Contacts list after a moment to see the new contacts. ### What happens to existing contacts Contacts are matched by email address, ignoring case. | Case | Result | | --- | --- | | The address is new | A contact is created. | | The address already exists | The contact is updated. Names are overwritten only when the file has a value. Custom field values from the file replace the stored ones, and a blank cell in a mapped custom field column clears that field. Other custom fields are kept. | | The address appears twice in the file | Both rows are applied in order, so the later row wins. | | The contact isn't in the selected audience yet | It joins the audience as subscribed. | | The contact was unsubscribed from the selected audience | It's subscribed to that audience again. | > **Imports resubscribe people:** Importing a contact into an audience it unsubscribed from subscribes it again. Before you import into an existing audience, remove people who opted out from your file, or import without picking that audience. A few more things to know: - The mapping list includes **Unsubscribed**, but the import doesn't apply it. To mark imported people as unsubscribed, select them afterwards on the Contacts list and use the **Unsubscribe** bulk action. - Imports don't send `contact.*` or `subscriber.*` webhook events, and they don't start automations such as **Added to audience**. - If an audience would go over its subscriber limit, the import is rejected with the limit in the message, for example "Pay as you go includes 10,000 subscribers per audience." See [Audiences](/docs/audiences/#limits). ## Export contacts 1. **Narrow the list.** On **Email Marketing → Contacts**, use search and **Filter** to show the contacts you want. With no search or filters, the export includes every contact. 2. **Export.** Select **Export** and choose **CSV** or **XLSX**. Your browser downloads `contacts.csv` or `contacts.xlsx`. An export can include up to **10,000 contacts**. If more contacts match, the export fails, so add filters, such as an audience or a creation date range, and export in parts. The file has one row per contact and these columns: | Column | Value | | --- | --- | | `email` | The contact's email address. | | `first_name`, `last_name` | The contact's names. | | `unsubscribed` | `true` if the marketing status is **Unsubscribed**, otherwise `false`. | | `audiences` | Names of every audience the contact belongs to, separated by `; `. | | One column per custom field key | The stored value. Multi select values are joined with `;`. | | `created_at`, `updated_at` | ISO 8601 timestamps. | ## Use the API The API has no file import. To add many contacts from code, call [Create a contact](/docs/api-reference/contacts/create/) for each one, or [Add a subscriber](/docs/api-reference/audiences/subscribers/add/) to create the contact and its audience membership in one call. ### Bulk actions [`POST /v2/contacts/bulk`](/docs/api-reference/contacts/bulk/) runs one action on up to 100 contacts, given by `con_` ID: | `action` | Effect | Needs `audience_id` | | --- | --- | --- | | `add_to_audience` | Adds the contacts to the audience. Contacts with `unsubscribed: true` join as unsubscribed. | Yes | | `remove_from_audience` | Deletes their membership in the audience. | Yes | | `unsubscribe` | Sets `unsubscribed: true` (marketing status **Unsubscribed**). | No | | `resubscribe` | Sets `unsubscribed: false`. | No | | `delete` | Deletes the contacts and their memberships. | No | ```bash curl https://api.emailit.com/v2/contacts/bulk \ -X POST \ -H "Authorization: Bearer $EMAILIT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "action": "add_to_audience", "ids": ["con_2kq8Vt4xLm7Rz", "con_9fW3pQ1nBc6Yt"], "audience_id": "aud_5hJ2kL8mNp4Qr" }' ``` ```json { "object": "contact_bulk", "action": "add_to_audience", "processed": 2, "ids": ["con_2kq8Vt4xLm7Rz", "con_9fW3pQ1nBc6Yt"] } ``` The request fails as a whole if `ids` has more than 100 entries (`400`) or if any contact or the audience doesn't exist (`404`, with the unknown IDs in `missing`). To process more contacts, page through [List contacts](/docs/api-reference/contacts/list/) and send batches of 100. ### Export [`GET /v2/contacts/export`](/docs/api-reference/contacts/export/) returns the same file as the dashboard. Set `format` to `csv` (the default) or `xlsx`, and add any [List contacts](/docs/api-reference/contacts/list/) filters, search and sort: ```bash curl "https://api.emailit.com/v2/contacts/export?format=csv&audience_id=aud_5hJ2kL8mNp4Qr&unsubscribed=false" \ -H "Authorization: Bearer $EMAILIT_API_KEY" \ -o contacts.csv ``` If more than 10,000 contacts match, the endpoint returns `422` with "Export is limited to 10000 contacts. Narrow your filters and try again." ## Related - [Custom fields](/docs/contacts/custom-fields/): Define the fields your columns map to. - [Subscribers](/docs/audiences/subscribers/): Manage who is on each audience. --- Source: https://emailit.com/docs/contacts/import-export/ --- # Contacts > Contacts are the people in your workspace. See how contacts, audiences and subscribers relate, what the Contacts list and profile show, and how opt-outs work. A contact is one person in your workspace, identified by their email address. Contacts hold the names and custom field values you use to personalize campaigns, and they join audiences, which are the lists campaigns send to. This page explains how the pieces fit together and what you can do with contacts in the dashboard and the API. ## Contacts, audiences and subscribers Emailit keeps people and lists apart. A person exists once as a contact, and each list they're on is a separate subscriber record with its own opt-in state. | | Contact | Audience | Subscriber | | --- | --- | --- | --- | | **What it is** | One person | A named list you send campaigns to | One contact's membership in one audience | | **ID prefix** | `con_` | `aud_` | `sub_` | | **Unique by** | Email address, per workspace | Name, per workspace | One per contact per audience | | **Holds** | Email, first name, last name, custom fields, marketing status | Name, subscriber count, subscribe URL | A **Subscribed** flag with subscribed and unsubscribed dates | | **Deleting it** | Also removes the contact from every audience | Removes all its subscribers. The contacts stay. | Removes that membership. The contact stays. | ```text Workspace ├── Contacts ────────────── one per email address │ ├── email, first_name, last_name, custom_fields │ └── marketing status (unsubscribed: true | false) └── Audiences ───────────── named lists, for example "Newsletter" └── Subscribers ─────── one per contact on the list └── subscribed: true | false ``` In practice: - One contact can be a subscriber of many audiences, with a separate **Subscribed** flag in each. - Adding someone to an audience by email creates the contact first if it doesn't exist yet. - Names and custom fields live on the contact. Editing them from an audience changes the contact, so the change shows everywhere. ### Who receives a campaign A campaign goes to every contact that meets all three conditions: 1. The contact is a subscriber of at least one of the campaign's audiences, with **Subscribed** on. 2. The contact's marketing status is **Subscribed**. 3. The address isn't on your [suppression list](/docs/suppressions/). A contact in several of the selected audiences gets one copy. See [Create a campaign](/docs/campaigns/create/) for the full rules. ## Marketing status Every contact also has a workspace-wide **Marketing status**: **Subscribed** or **Unsubscribed** (the `unsubscribed` field in the API). It's a global opt-out that sits above the per-audience flag. | You change | How | Effect on campaigns | | --- | --- | --- | | Marketing status | Bulk **Unsubscribe** or **Resubscribe** on the Contacts list, or `unsubscribed` in the API | **Unsubscribed** contacts are skipped in every audience. Their audience memberships don't change. | | One audience subscription | **Unsubscribe** or **Resubscribe** on the contact page or in the audience, or `subscribed` on the subscriber | The contact is skipped only for campaigns to that audience. | | The recipient clicks the unsubscribe link | The hosted unsubscribe page | The contact is unsubscribed from every audience it belongs to. | Marketing status and audience subscriptions control campaigns only. They don't block emails you send with the API or SMTP, or emails sent by automations. To stop all mail to an address, add it to your [suppressions](/docs/suppressions/). [Unsubscribes](/docs/audiences/unsubscribes/) covers every opt-out path. ## The Contacts list Open **Email Marketing → Contacts** to see every contact in the workspace, newest first. - **Columns.** **Email** is always shown. Turn **First name**, **Last name**, **Audiences**, **Created** and **Updated** on or off under **Display options > Edit columns**, or select **Show full name** to merge the names into one **Name** column. In the **Audiences** column, each audience badge is green while the contact is subscribed to it and red after they unsubscribe. - **Search** matches email, first name and last name. - **Filter** by Email, Name, First name, Last name, Unsubscribed, Created, Updated, Audiences or Audience, and by any [custom field](/docs/contacts/custom-fields/). With two or more filters, choose whether to match all of them or any of them. - **Sort** by selecting a column header. - **Import** and **Export** move contacts in and out as files. See [Import and export contacts](/docs/contacts/import-export/). Select a row to open the contact. The row menu has **Edit** and **Delete**. ### Add a contact Select **Add contact** and enter the **Email**, and optionally **First name**, **Last name** and **Audiences**. Select **Show custom fields** to fill in custom field values. An email address can only exist once per workspace, so adding an existing address fails. To change a contact later, select **Edit**. You can change the names and custom fields. The email address can't be changed in the dashboard. Use the `email` field of [Update a contact](/docs/api-reference/contacts/update/) instead. ### Bulk actions Select contacts with the checkboxes (the header checkbox selects the whole page), then open **Actions**. Each action applies to up to 100 contacts at a time. | Action | What it does | | --- | --- | | **Add to audience** | Adds the contacts to the audience you pick. Existing memberships stay as they are. Contacts whose marketing status is **Unsubscribed** join as unsubscribed subscribers. | | **Remove from audience** | Deletes their membership in the audience you pick. The contacts stay. | | **Unsubscribe** | Sets their marketing status to **Unsubscribed**. | | **Resubscribe** | Sets their marketing status back to **Subscribed**. Audience subscriptions don't change. | | **Delete** | Deletes the contacts and all their memberships. This can't be undone. | ## The contact page The contact page shows everything Emailit knows about one person. Use **Edit** and **Delete** at the top. | Section | What it shows | | --- | --- | | **Details** | Email, first name, last name, marketing status, created and updated dates. | | **Custom fields** | Every custom field defined in the workspace and the contact's value. | | **Metrics and Insights** | **Emails sent**, **Loads**, **Clicks**, **Last activity**, **Subscribed audiences** (subscribed out of total) and **Marketing status**. The counts include every outgoing email sent to the address, not only campaigns. | | **Audiences** | Each audience the contact belongs to, with its status and subscribed and unsubscribed dates. The row menu can **Unsubscribe** or **Resubscribe** the contact for that audience, or **Remove from audience**. **Add to audience** adds a new membership. | | **Sent emails** | Outgoing emails sent to the address, with subject, status, campaign, loads, clicks and send time. | | **Activity log** | A timeline of events for the contact, such as "Contact created", "Added to Newsletter", "Unsubscribed from Newsletter" and "Email delivered". | ## Use the API The [Contacts API](/docs/api-reference/contacts/) covers everything the list and profile do, except importing files: - [Create](/docs/api-reference/contacts/create/), [retrieve](/docs/api-reference/contacts/get/), [update](/docs/api-reference/contacts/update/), [list](/docs/api-reference/contacts/list/) and [delete](/docs/api-reference/contacts/delete/) contacts. Wherever an ID is expected, you can pass the contact's email address instead of its `con_` ID. - Pass `audiences` (an array of `aud_` IDs) when you create a contact to subscribe it right away. On update, `audiences` replaces the contact's memberships. - [Run a bulk action](/docs/api-reference/contacts/bulk/) on up to 100 contacts, and [export](/docs/api-reference/contacts/export/) up to 10,000 as CSV or XLSX. ```bash curl https://api.emailit.com/v2/contacts \ -X POST \ -H "Authorization: Bearer $EMAILIT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "email": "ada@example.com", "first_name": "Ada", "last_name": "Lovelace", "custom_fields": { "company": "Acme", "plan": "pro" }, "audiences": ["aud_2kq8Vt4xLm7Rz"] }' ``` Creating a contact with an email that already exists returns `409` with the existing contact in `existing`. The Contacts API needs an API key with **Full Access**. Contact changes produce [`contact.created`, `contact.updated` and `contact.deleted`](/docs/webhooks/events/contact/) events, and membership changes produce [`subscriber.*`](/docs/webhooks/events/subscriber/) events, which you can receive with [webhooks](/docs/webhooks/). ## Next steps - [Custom fields](/docs/contacts/custom-fields/): Store extra data on contacts and use it in filters and emails. - [Import and export](/docs/contacts/import-export/): Bring contacts in from a CSV or Excel file, and download them. - [Audiences](/docs/audiences/): Group contacts into lists you can send campaigns to. - [Unsubscribes](/docs/audiences/unsubscribes/): How opt-outs work and how to respect them. --- Source: https://emailit.com/docs/contacts/ --- # Data retention > How long Emailit keeps message contents, activity, logs and analytics on each plan, how to set custom windows, and what stops working after deletion. Emailit keeps four kinds of email data for a limited time, then deletes it permanently. This page explains each data type, the default windows on each plan, how to choose your own windows, and what you can't do once data is gone. ## Data types These names match **Workspace → Settings → Data retention**. | Type | What it covers | Where you see it | | --- | --- | --- | | **Message contents** | Raw email bodies and headers | **Show Preview** on an email, attachments, and the API's body, raw and attachment endpoints | | **Message activity** | Delivery records, opens, clicks and message metadata | The Emails list and email details, deliveries, loads and clicks | | **Logs** | API and SMTP request logs, plus delivery-attempt and webhook logs | **Email API → Logs**, **Email API → Events** and a webhook's **Requests** tab | | **Analytics** | Aggregated sending statistics and reports | **Email API → Analytics** | Retention doesn't apply to contacts, audiences, templates, domains, suppressions or settings. They're kept until you delete them. Email verification results expire separately, 30 days after they're created. ## Plan defaults | | Pay as you go | Pro | Business | Custom | | --- | --- | --- | --- | --- | | Message contents kept | 7 days | 30 days | 30 days | Flexible | | Message activity kept | 30 days | 365 days | 365 days | Flexible | | Request logs kept | 7 days | 30 days | 30 days | Flexible | | Analytics kept | 365 days | Forever | Forever | Flexible | **Forever** means Emailit doesn't delete that data type on a schedule. On Custom plans, retention is agreed with the Emailit team. Retention follows the workspace's current plan. When you upgrade, the longer windows apply from then on, but data already deleted under the old plan doesn't come back. ## Custom retention With the Data retention add-on, you can set your own window for each data type, from 1 to 365 days. Windows can be longer or shorter than the plan default, so you can also use the add-on to keep less data. 1. **Activate the add-on.** Go to **Workspace → Billing** and, under **Add-ons**, select **Activate** on **Data retention**. It costs $50 a month per workspace. See [Add-ons](/docs/billing/add-ons/#data-retention). 2. **Open Data retention.** Go to **Workspace → Settings → Data retention**. Each selector shows the current plan default, for example **Plan default (7 days)**. 3. **Choose windows.** For each type, pick **Plan default** or 1, 3, 7, 14, 30, 60, 90, 180 or 365 days. 4. **Save.** Select **Save**. You see "Data retention updated". Only admins can change retention. Without the add-on, the page shows "Custom data retention is not available", names your plan and disables the selectors, and your plan defaults apply. Picking **Plan default** again removes your custom value for that type. An [AppSumo data retention license](/docs/billing/appsumo/#data-retention-license) unlocks the same settings. ## How deletion works Emailit runs its retention cleanup once a day. Each run permanently deletes data older than the window for that type. Shortening a window deletes older data at the next run, and nothing can restore it. > **Kept for compliance:** Bounced and suppressed message data is always retained for compliance purposes, regardless of these settings. ## What stops working after deletion | After this is deleted | What changes | | --- | --- | | **Message contents** | Email details still show the status, recipients and events, but **Show Preview** has nothing to show. The API returns no body, attachments or raw message. **Retry** and **Forward** fail with `422`, for example "Email raw content has been purged and can no longer be retried." | | **Message activity** | The email no longer appears in the Emails list, the API or campaign drill-downs. | | **Logs** | Request logs, events and webhook request history for that period are gone, so you can't inspect or replay those webhook requests. | | **Analytics** | Charts can't show the deleted period. Date ranges older than your analytics window are shortened to fit it. | If you need email content for longer, store it on your side when you send, or listen to [webhooks](/docs/webhooks/) and keep the events you need. ## GDPR and deletion requests Short retention windows reduce the personal data Emailit stores for you. To act on a data subject request: - Delete the person's contact and subscriber records in the dashboard or with the [Contacts API](/docs/api-reference/contacts/). Deleting a contact removes it from every audience. - Keep a suppression entry if they must never be emailed again. See [Suppressions](/docs/suppressions/). - For message data that hasn't reached its retention window yet, email [support@emailit.com](mailto:support@emailit.com) with the workspace ID and the email address concerned. See [Security and compliance](/docs/security/) for more on how Emailit handles your data. ## Related - [Limits and quotas](/docs/limits/) - [Add-ons](/docs/billing/add-ons/) - [Email details](/docs/logs/email-details/) --- Source: https://emailit.com/docs/data-retention/ --- # Email deliverability best practices > A practical checklist for reaching the inbox: authentication and DMARC alignment, consent, unsubscribes, list hygiene, content and Gmail and Yahoo sender rules. This guide collects the practices that matter most for inbox placement and shows how to apply each one with Emailit. Work through it when you set up a new domain, and come back to it when your bounce or complaint rates climb. ## Authenticate everything Emailit handles SPF and DKIM for you once your domain is verified. Add DMARC yourself. 1. **Verify every domain you send from.** Each subdomain is separate. See [Add a domain](/docs/domains/add-a-domain/). 2. **Publish a DMARC record.** Start with `v=DMARC1; p=none;` on `_dmarc.`. Gmail, Yahoo and Outlook require DMARC from bulk senders. 3. **Collect DMARC reports.** On Pro and higher, turn on [DMARC reports](/docs/dmarc/set-up/) to see every service that sends as your domain. 4. **Move to enforcement.** Once reports show that all your legitimate mail passes, step up to `p=quarantine` and then `p=reject`. See [Read DMARC reports](/docs/dmarc/reports/#move-to-enforcement). ### How alignment works with Emailit DMARC passes when SPF or DKIM passes **and** the domain it checked matches your `From` domain. | Check | Domain checked | Aligned with `From: hello@acme.com`? | | --- | --- | --- | | DKIM | `d=acme.com` | Yes, in both relaxed and strict mode. | | SPF | Return path `emailit.acme.com` | Yes in relaxed mode (the default). No if you set `aspf=s`. | Leave `aspf` at its default (relaxed). If you need strict alignment, DKIM still aligns, so DMARC keeps passing. ## Separate your mail streams Use a different subdomain for each kind of mail so that each one builds its own reputation: | Stream | Example domain | Sent with | | --- | --- | --- | | Transactional: receipts, password resets, alerts | `notify.acme.com` | API or SMTP | | Marketing: newsletters, promotions | `news.acme.com` | Campaigns | If a campaign draws complaints, your password resets keep landing in the inbox. Pair this with [sending-only API keys](/docs/developers/api-keys/) restricted to one domain, so each app can only send from its own stream. ## Get consent and make leaving easy - **Only mail people who asked for it.** Don't buy, rent or scrape lists. Cold email isn't allowed on Emailit. - **Make unsubscribing easy.** Put an unsubscribe link (`{{unsubscribe_url}}`) in every campaign. The campaign's **Send** step flags campaigns without one. Emailit also adds `List-Unsubscribe` and `List-Unsubscribe-Post` headers to campaign emails, so Gmail and Yahoo show their own unsubscribe button and can unsubscribe the recipient in one click. - **Add the headers to marketing mail you send through the API.** Emailit only adds them to campaigns. For newsletters you send with the API or SMTP, set your own headers: ```json { "headers": { "List-Unsubscribe": ", ", "List-Unsubscribe-Post": "List-Unsubscribe=One-Click" } } ``` Your URL must accept a `POST` and unsubscribe the person without further steps. See [Headers and metadata](/docs/email-api/headers-and-metadata/). - **Honor unsubscribes quickly.** Gmail and Yahoo expect requests to take effect within two days. Campaign unsubscribes take effect immediately. For other mail, stop sending as soon as you receive the request, for example by [adding a suppression](/docs/suppressions/manage/). ## Keep your list clean - **Verify lists you didn't collect yourself,** such as imports from an old system, before the first send. See [Verify a list](/docs/email-verification/lists/). - **Check addresses at sign-up.** Call [single verification](/docs/email-verification/single/) when someone enters an address, and ask them to fix typos. - **Keep automatic suppression on.** It stops you from mailing addresses that bounced or complained. See [Suppressions](/docs/suppressions/). - **Retire inactive recipients.** People who haven't opened or clicked in six months or more drag down engagement. Send them a re-engagement email, then stop mailing those who don't respond. - **Watch for spikes.** A sudden jump in bounces usually points to one bad list or import. Pause it and investigate before it pushes your workspace into **At risk**. ## Write content that looks like real mail - **Send a plain-text part** alongside the HTML. - **Use a consistent sender.** Keep the same `From` name and address for each stream, and use a `Reply-To` that someone reads. - **Balance images and text.** An email that's a single large image with little text often lands in spam. - **Link to your own domain.** Avoid URL shorteners and links to domains with a poor reputation. - **Keep HTML under about 100 KB.** Gmail clips longer messages and hides the rest, including your unsubscribe link. The campaign builder warns you before you send. - **Avoid attachments in bulk mail.** Link to the file instead. Never attach executables or archives inside archives. - **Check the score.** Emailit shows the spam checks that fired on every message. See [Spam checks](/docs/deliverability/spam-checks/). ## Meet the bulk sender requirements Gmail and Yahoo apply these rules to senders of roughly 5,000 or more messages a day to their users, and Outlook.com applies similar ones. They're good practice at any volume. | Requirement | How it's covered | | --- | --- | | SPF and DKIM pass | Automatic once your domain is verified. | | DMARC record, at least `p=none` | Publish `_dmarc.`. See [DNS records](/docs/domains/dns-records/#dmarc-txt-optional). | | `From` domain aligned with SPF or DKIM | Automatic. DKIM always aligns, and SPF aligns in relaxed mode. | | One-click unsubscribe for marketing mail | Automatic for campaigns. Add the headers yourself for marketing mail sent through the API or SMTP. | | Visible unsubscribe link in marketing mail | Add `{{unsubscribe_url}}` to every campaign, and a link to your own marketing templates. | | Unsubscribes honored within two days | Immediate for campaigns. Your responsibility for other mail. | | Spam complaint rate below 0.3% | Aim for under 0.1%. Watch complaints in [Analytics](/docs/analytics/) and [Google Postmaster Tools](https://postmaster.google.com). | | TLS for delivery | Emailit delivers over TLS whenever the receiving server supports it. | ## Monitor and react - **Sending health.** Check your 0–100 score on the **Dashboard**. Act when it drops below 80. See [Sending health](/docs/deliverability/sending-health/). - **Bounces and complaints.** Use [Analytics](/docs/analytics/) and subscribe to `email.bounced` and `email.complained` [webhooks](/docs/webhooks/event-types/) to react in your app. - **DMARC reports.** Find services that send as your domain without authentication. - **Google Postmaster Tools.** Verify your domain there to see the spam rate and reputation Gmail reports for it. ## Related - [Bounces and complaints](/docs/deliverability/bounces-and-complaints/): How Emailit handles each kind of failure. - [Warm up a domain or IP](/docs/deliverability/warm-up/): Grow volume without tripping filters. --- Source: https://emailit.com/docs/deliverability/best-practices/ --- # Bounces and complaints > How Emailit handles hard and soft bounces, retries temporary failures, processes late bounces and spam complaints, and suppresses addresses automatically. When a message can't be delivered, or a recipient marks it as spam, Emailit records what happened, updates the email's status and, in most cases, stops you from mailing that address again. This page explains each kind of failure, the retry schedule and the automatic suppression rules. ## Hard and soft bounces Emailit sorts each failed delivery attempt by the receiving server's reply. | Type | Typical replies | What Emailit does | Status | | --- | --- | --- | --- | | **Hard bounce** (permanent) | `550` mailbox unavailable, `551` user not local, `553` mailbox name not allowed, `554` transaction failed | Stops trying. | `bounced` | | **Soft bounce** (temporary) | `421` service not available, `450` mailbox busy, `451` local error, `452` insufficient storage, timeouts, connection errors | Retries later. | `attempted` | Some temporary `4xx` replies describe a mailbox that won't accept mail, such as "mailbox full", "over quota" or "account disabled". Emailit recognizes common wording like this and treats it as a hard bounce. Unknown errors are treated as temporary and retried. For what each code means, see [SMTP reply codes](/docs/dictionary/smtp-reply-codes/) and [Bounce categories](/docs/dictionary/bounce-categories/). The full reply for each attempt is on the email's **Deliveries** tab in **Email API → Emails**. ## Retries After a temporary failure, Emailit retries with a growing delay. A message gets up to 7 attempts over about 21 hours: | After attempt | Next try in | | --- | --- | | 1 | 10 minutes | | 2 | 20 minutes | | 3 | 40 minutes | | 4 | 80 minutes | | 5 | 160 minutes | | 6 | 320 minutes | | 7 | 640 minutes, then Emailit stops and bounces the message | While it's retrying, the email's status is `attempted` and each failed try fires an `email.attempted` event with the receiver's reply. If the last attempt also fails, the email becomes `bounced` with the note "Maximum number of delivery attempts (7) has been reached" and the address is suppressed. ### Back-off when receivers push back If a provider rate-limits one of Emailit's sending IPs (a `451` reply about too many messages), Emailit pauses delivery from that IP to that provider for 5 minutes. If the provider blocks the IP (`550 5.7.1`), it pauses for an hour. Affected messages stay `attempted` and go out when the pause ends, so you don't need to do anything. ## Late bounces Some servers accept a message and only report later that they couldn't deliver it. They send a delivery status notification (DSN) to the message's return path, `emailit.`, which is why that MX record is required. Emailit matches the report to the original message, changes its status to `bounced`, fires `email.bounced` and suppresses the address. ## Complaints When a recipient selects **Report spam**, many mailbox providers send a report back to the sender through a feedback loop. Emailit receives these reports in the standard ARF format and in Outlook's JMRP format, matches them to the original message, then: - sets the email's status to `complained`, - fires `email.complained`, - adds the address to your suppression list with type `complaint`. Gmail doesn't send individual complaint reports to senders. To see your Gmail spam rate, use [Google Postmaster Tools](https://postmaster.google.com). Keep your complaint rate under 0.1%, and never let it reach 0.3%. ## Statuses and events | Status | Event | Meaning | | --- | --- | --- | | `attempted` | [`email.attempted`](/docs/webhooks/events/email/attempted/) | A temporary failure. Emailit will retry. | | `bounced` | [`email.bounced`](/docs/webhooks/events/email/bounced/) | A permanent failure, a late bounce, or all retries used. | | `complained` | [`email.complained`](/docs/webhooks/events/email/complained/) | The recipient reported the message as spam. | | `suppressed` | [`email.suppressed`](/docs/webhooks/events/email/suppressed/) | Not sent, because the address is on your suppression list. | `bounced`, `complained` and `suppressed` are final: a later event doesn't change them. See [Email statuses](/docs/logs/email-statuses/) for the full list. ## Automatic suppression Emailit adds an address to your suppression list in these cases: | Trigger | Suppression type | Reason | | --- | --- | --- | | A late bounce report (DSN) arrives | `recipient` | `bounce` | | All 7 delivery attempts fail | `recipient` | `too many soft fails` | | A second hard bounce to the same address within 24 hours | `recipient` | `too many bounces` | | A spam complaint arrives | `complaint` | `complaint` | A single immediate hard bounce doesn't suppress the address on its own. A second one within 24 hours does. What each type blocks: - **`recipient`** blocks every send to the address: API, SMTP, campaigns and automations. The email gets the status `suppressed` instead of being sent. - **`complaint`** stops campaigns from sending to the address. Transactional email through the API and SMTP still goes out, so receipts and password resets keep working. When a message to an address is delivered successfully, Emailit removes that address's `recipient` suppression. See [Suppressions](/docs/suppressions/) for how suppressions work and how to remove one. ### Choose what gets suppressed On Pro and Business, workspace admins can change automatic suppression in **Workspace → Settings** on the **Suppressions** tab: | Setting | Options | | --- | --- | | **Enable automatic suppression** | On (default) or off. | | **Suppress on** | **Bounces and Complaints** (default), **Bounces only** or **Complaints only**. | On other plans, automatic suppression is always on for bounces and complaints. The controls are visible but locked. | | Pay as you go | Pro | Business | Custom | | --- | --- | --- | --- | --- | | Configurable automatic suppression | — | Included | Included | — | > **Turning automatic suppression off can hurt your reputation:** If you turn it off, Emailit keeps sending to addresses that bounced or complained. Your bounce rate rises, which lowers your [sending health](/docs/deliverability/sending-health/). Only do this if your own system suppresses those addresses, for example by listening to `email.bounced` and `email.complained` webhooks. ## Bounce-rate thresholds Bounces feed into your sending health score. Emailit measures the share of unique recipients that bounced over the last 7, 30 and 90 days: | Bounce rate | Effect | | --- | --- | | Under 2% | Healthy. Pro and Business can get automatic limit raises. | | 2% to 4% | Healthy, but no automatic limit raises. | | Above 4% | At risk. | | Above 5% on a domain | The domain is paused. | | 6% or more on the workspace | The workspace is suspended. | See [Sending health](/docs/deliverability/sending-health/) for how the score is calculated and how to recover. ## Related - [Manage suppressions](/docs/suppressions/manage/): View, add, import and remove suppressions. - [Email verification](/docs/email-verification/): Catch bad addresses before you send. - [SMTP reply codes](/docs/dictionary/smtp-reply-codes/): What each reply code means. - [Bounce categories](/docs/dictionary/bounce-categories/): How bounces are grouped. --- Source: https://emailit.com/docs/deliverability/bounces-and-complaints/ --- # Dedicated IPs > The difference between Emailit's shared IP pools and a dedicated IP, when a dedicated IP is worth it, and how to request one and warm it up. By default, Emailit sends your mail from shared pools of IP addresses. You can also get one or more IP addresses that only your workspace uses. This page explains the difference, when a dedicated IP makes sense and how to request one. ## Shared pools and dedicated IPs | | Shared IP pool | Dedicated IP | | --- | --- | --- | | Who sends from it | Many Emailit customers | Only your workspace | | IP reputation | Shared. Emailit keeps it clean with spam checks, automatic suppression and sending health limits. | Yours alone. It reflects only your sending. | | Warm-up | Not needed. The IPs are already warm. | Needed. A new IP has no history. | | Volume needed | Any | Steady volume, roughly 1,000 emails a day or more | | Cost | Included | Paid add-on. See [pricing](/pricing/). | With either option, your domain's reputation is your own. SPF, DKIM and DMARC work the same way, and you don't need to change any DNS records. ## When a dedicated IP makes sense A dedicated IP helps when: - you send a **steady volume**, at least around 1,000 emails a day, every day, - you want your IP reputation to depend **only on your own sending**, - a customer or partner needs to **allowlist a fixed IP address** for your mail. Stay on the shared pool when: - your volume is low or comes in bursts, such as a monthly newsletter. An IP that sends rarely looks unfamiliar to providers each time, and inbox placement suffers, - you're still building your sending reputation or cleaning up your list. Problems are easier to recover from on a warm shared pool. ## Request a dedicated IP 1. **Open Billing.** Go to **Workspace → Billing**. In the **Add-ons** table, find **Dedicated IP** and select **Contact sales**. 2. **Fill in the form.** Choose your **Company size**, **Annual revenue**, **Annual email volume** and **How many dedicated IPs** (1 to 4, or 5 or more). In **Describe your needs**, write at least 10 characters about what you send and why you want a dedicated IP. 3. **Submit the request.** The Emailit team reviews it and replies. You can follow the request in **Workspace → Settings** on the **Requests** tab. Its status changes from **Pending Review** to **Awaiting Your Reply**, **Approved** or **Rejected**, and you can reply to the team in the request's conversation. A workspace can have one pending dedicated IP request at a time. See [Requests](/docs/workspaces/requests/). ## After approval Emailit assigns the IP addresses to your workspace, and all mail from the workspace is sent from them. You don't need to change your code, SMTP settings or DNS records. A new dedicated IP starts with an automatic warm-up. Emailit caps its daily volume, starting at 500 emails and resetting each day to about 1.5 times what the IP sent the day before. Ramp up your own sending to match, starting with your most engaged recipients. See [Warm up a domain or IP](/docs/deliverability/warm-up/). ## Related - [Add-ons](/docs/billing/add-ons/): Dedicated IPs and data retention. - [Warm up a domain or IP](/docs/deliverability/warm-up/): Build reputation on a new IP. --- Source: https://emailit.com/docs/deliverability/dedicated-ips/ --- # Deliverability > What decides whether your email reaches the inbox, what Emailit handles for you automatically, and where to start improving your inbox placement. Deliverability is the share of your email that lands in the inbox rather than the spam folder or nowhere at all. This section explains what mailbox providers look at, what Emailit does for you on every message, and what's left for you to manage. ## What mailbox providers look at Gmail, Outlook, Yahoo and other providers decide where a message goes based on four things: | Area | What it means | Where to learn more | | --- | --- | --- | | **Authentication** | SPF, DKIM and DMARC prove the message really comes from your domain. | [Sending domains](/docs/domains/), [DMARC reports](/docs/dmarc/) | | **Reputation** | Your domain's and sending IP's history: bounce rates, complaints and how recipients engage. | [Sending health](/docs/deliverability/sending-health/), [Warm up](/docs/deliverability/warm-up/) | | **List hygiene** | Sending only to valid addresses of people who asked for your mail. | [Suppressions](/docs/suppressions/), [Email verification](/docs/email-verification/) | | **Content** | Subject, HTML, links and attachments that look like legitimate mail, not spam. | [Spam checks](/docs/deliverability/spam-checks/) | Authentication is a yes-or-no gate. Reputation and list hygiene build up over weeks, and you control them through who you send to and how often. ## What Emailit does automatically On every message, without any setup beyond verifying your domain: - **Signs with DKIM.** Each message gets a DKIM signature with your domain's 2048-bit key, so it aligns with your `From` domain for DMARC. - **Sets the return path.** Bounces go to `emailit.`, which is covered by SPF and aligns with your domain under DMARC's relaxed alignment. - **Scores content before sending.** Emailit checks each signed message with Rspamd. Messages that score 7 or higher are held instead of sent. See [Spam checks](/docs/deliverability/spam-checks/). - **Retries temporary failures.** Messages that get a temporary error are retried up to 7 times over about 21 hours. - **Backs off when receivers push back.** When a provider rate-limits or blocks a sending IP, Emailit pauses delivery to that provider from that IP for a while instead of hammering it. - **Classifies bounces and complaints.** Permanent failures, late bounce reports and spam complaints from feedback loops are matched to the original message and update its status. See [Bounces and complaints](/docs/deliverability/bounces-and-complaints/). - **Suppresses bad addresses.** Addresses that bounce or complain are added to your suppression list, so you don't keep mailing them. See [Suppressions](/docs/suppressions/). - **Monitors sending health.** Emailit tracks the bounce rate of your workspace and each domain, and pauses a domain before it does lasting damage. See [Sending health](/docs/deliverability/sending-health/). - **Re-checks DNS daily.** If a domain's records break, Emailit stops sending from it and emails the workspace owner. - **Adds unsubscribe headers to campaigns.** Campaign emails include `List-Unsubscribe` and one-click unsubscribe headers. ## What you manage - Publish a DMARC record and move it toward enforcement over time. - Send only to people who opted in, and remove inactive addresses. - Verify lists you didn't collect yourself before you mail them. - Keep your bounce rate under 2% and your spam complaint rate under 0.1%. - Ramp up volume gradually on new domains and IPs. [Email best practices](/docs/deliverability/best-practices/) turns this into a checklist. ## In this section - [Best practices](/docs/deliverability/best-practices/): Authentication, consent, list hygiene, content and bulk sender rules. - [Sending health](/docs/deliverability/sending-health/): Your 0–100 score, bounce thresholds and automatic limit raises. - [Spam checks](/docs/deliverability/spam-checks/): How Emailit scores content and what to do with held email. - [Bounces and complaints](/docs/deliverability/bounces-and-complaints/): Hard and soft bounces, retries, feedback loops and suppression rules. - [Warm up a domain or IP](/docs/deliverability/warm-up/): A ramp schedule for new senders. - [Dedicated IPs](/docs/deliverability/dedicated-ips/): When to move off shared IPs and how to request one. --- Source: https://emailit.com/docs/deliverability/ --- # Sending health > How Emailit scores sending health from 0 to 100 using bounce rates, what Healthy, At risk, Paused and Suspended mean, and how healthy sending raises limits. Sending health is Emailit's measure of how cleanly your workspace and each of its domains send. It's based on the share of recipients whose mail bounces. This page explains how the score is calculated, what each status means, the thresholds Emailit applies and how to recover. ## How the score works Emailit recalculates sending health every hour, for the workspace as a whole and for each sending domain. 1. **It counts unique recipients.** For each time window, Emailit counts the distinct addresses you sent to and how many of them bounced. Sending ten emails to the same bad address counts as one bounced recipient. 2. **It looks at several windows.** The last 7, 30 and 90 days, plus everything since your first send within those 90 days. 3. **It ignores small windows.** A window needs at least 100 unique recipients to count. 4. **The worst window wins.** Of the windows that count, the one with the highest bounce rate sets your score and status. Until at least one window reaches 100 unique recipients, the status is **Collecting** and there's no score yet. ### Score bands The score is stepped, not a straight percentage: | Bounce rate | Score | | --- | --- | | Under 0.5% | 100 | | 0.5% to under 1% | 96 | | 1% to under 1.5% | 92 | | 1.5% to under 2% | 88 | | 2% to under 2.5% | 80 | | 2.5% to under 3% | 72 | | 3% to under 3.5% | 64 | | 3.5% to under 4% | 56 | | 4% to under 4.5% | 48 | | 4.5% to under 5% | 40 | | 5% to under 5.5% | 28 | | 5.5% to under 6% | 16 | | 6% to under 8% | 8 | | 8% or more | 0 | The dashboard shows scores of 80 and above in green, 50 to 79 in amber and below 50 in red. ## Statuses and thresholds | Status | Applies to | When | What happens | | --- | --- | --- | --- | | **Collecting** | Workspace, domain | No window has 100 unique recipients yet. | Nothing. Keep sending. | | **Healthy** | Workspace, domain | Bounce rate is 4% or lower. | Nothing. Pro and Business workspaces can get [automatic limit raises](#automatic-limit-raises). | | **At risk** | Workspace, domain | Bounce rate is above 4%. | A warning on the dashboard. Emailit can also email the workspace owner. | | **Paused** | Domain | The domain's bounce rate is above 5%. | Sending from that domain stops. | | **Suspended** | Workspace | The workspace's bounce rate is 6% or higher. | All sending from the workspace stops. | These are the rules Emailit applies. Pausing, suspension, owner emails and automatic limit raises may be rolled out gradually, so a workspace or domain over a threshold can stay **At risk** for a while before it's paused or suspended. Don't treat that as room to spare. ### What a paused domain looks like - The domain page shows a **Sending paused** banner: "Sending from this domain is paused. Contact support to restore it." - The API rejects sends from it with `403 Domain paused`, and SMTP replies `550 Sending from this domain is paused`. - Mail from the domain that's already queued gets the status `held`. Other domains in the workspace keep sending. ### What a suspended workspace looks like - A **Workspace suspended** banner appears across the dashboard. - The API rejects requests that change data with `403 Workspace is suspended`, and SMTP replies `535 Mail server has been suspended`. - Queued mail gets the status `held`. ## Where to see it | Place | What it shows | | --- | --- | | **Dashboard** | The workspace score, status and a score history chart. | | **Workspace → Settings**, **General** tab | The full **Sending health** card: score, status, bounce rate, your weakest domain and history by day, week or month. | | **Email API → Domains** | A **Sending health** column with each domain's status. | | Domain page, **Sending health** tab | The domain's own score and history. Available on Pro, Business and Custom. | Sending health isn't available through the public API. ## Automatic limit raises New workspaces start at 2 emails per second and 5,000 emails per day. On Pro and Business, Emailit raises these limits automatically when your sending is healthy. Emailit checks every hour and raises the limits when all of these are true: - every window with at least 100 unique recipients has a bounce rate **under 2%**, - no domain in the workspace is paused and the workspace isn't suspended, - the last raise was at least 7 days ago. Each raise moves both limits one step up these ladders: | Limit | Steps | | --- | --- | | Emails per day | 500 → 1,000 → 2,000 → 5,000 → 10,000 → 25,000 → 50,000 → 100,000 → 250,000 → 500,000 → 1,000,000, then +25% per raise up to 2,000,000 | | Emails per second | 2 → 5 → 10 → 20 → 50 → 100 → 200 → 500 → 1,000, then +25% per raise up to 2,000 | On Pay as you go, or if you need more than the next step, request an increase from the **Sending Limits** card on the **Dashboard** with **Request Increase**. See [Limits](/docs/limits/). | | Pay as you go | Pro | Business | Custom | | --- | --- | --- | --- | --- | | Sending limit increases | On request | Automatic, based on sending health | Automatic, based on sending health | By agreement | ## Recover from a low score 1. **Find the source.** The **Sending health** card names your weakest domain. In **Email API → Emails**, filter by **Status** `bounced` and by **Domain** to see which recipients and which send caused it. 2. **Stop the bad send.** Pause the campaign, import or automation that's mailing the bouncing addresses. 3. **Clean the list.** Run the list through [email verification](/docs/email-verification/lists/) and remove `invalid`, `disabled` and `disposable` results. Keep [automatic suppression](/docs/suppressions/) on. 4. **Dilute with good mail.** The score is a rate over 7 to 90 days. Sending to engaged, valid recipients brings it down over time. The 7-day window recovers fastest, but the worst window decides your status. 5. **Contact support for a paused domain or suspended workspace.** Explain what caused the bounces and what you changed. Paused domains and suspended workspaces are restored by the Emailit team. ## Related - [Bounces and complaints](/docs/deliverability/bounces-and-complaints/): What counts as a bounce and how Emailit retries. - [Best practices](/docs/deliverability/best-practices/): Keep your bounce rate low from the start. --- Source: https://emailit.com/docs/deliverability/sending-health/ --- # Spam checks > How Emailit scores every outgoing email with Rspamd, why messages scoring 7 or more are held, how to read Spam Checks and how to fix and resend. Emailit scores every email it sends with Rspamd, the same kind of spam filter many receiving servers use. Messages that look too much like spam are held instead of sent, which protects your domain's reputation. This page explains how scoring works, how to read the results and what to do when a message is held. ## How scoring works 1. **Emailit prepares the message.** It adds its headers, such as `Message-ID` and the return path, and signs the message with your DKIM key. 2. **Rspamd scores the exact bytes that would be sent.** Each rule that matches adds to or subtracts from the score. 3. **Emailit compares the total with the threshold of 7.** - **Under 7:** the message is sent. - **7 or more:** the message gets the status `held` and isn't sent. Its delivery history says: "Held because Rspamd scored this message 8.4, which is at or above the threshold of 7." This applies to every outgoing message: API, SMTP, campaigns and automations. Inbound email isn't scored. If Rspamd is unavailable, Emailit sends the message without a score rather than delaying it. Those messages show **Not inspected**. ## Read the results ### On the email page Open a message in **Email API → Emails**. The header shows its score as a colored badge, such as **Score +3.20**: | Color | Score | Meaning | | --- | --- | --- | | Green | Under 5 | Low spam likelihood. | | Orange | 5 to under 7 | Close to the threshold. Worth improving. | | Red | 7 or more | Held. | | Grey | **Not inspected** | The message wasn't scored. | The **Spam Checks** panel lists every rule that matched, with three columns: | Column | Description | | --- | --- | | **Check** | What the rule looks for. Hover to see Rspamd's rule name. | | **Result** | **Negative** for rules that add to the score (bad), **Positive** for rules that subtract from it (good), **Neutral** for rules with no effect. | | **Score** | How much the rule added or removed, for example `+2.50` or `-0.10`. | Use the **All**, **Negative**, **Positive** and **Neutral** filter to focus on the rules that pushed the score up. ### In the email list The **Spam score** column in the Emails list shows each message's badge. Add a **Spam score** filter, for example *greater than or equal to* 5, to find messages that are close to being held. With the API, [List emails](/docs/api-reference/emails/list/) accepts the same filter: ```bash curl -G https://api.emailit.com/v2/emails \ -H "Authorization: Bearer $EMAILIT_API_KEY" \ --data-urlencode "spam_score.gte=5" ``` ## Common rules and fixes Rule names come from Rspamd. Their exact scores can change as the rules are updated. | Rule | What it means | Fix | | --- | --- | --- | | `MIME_HTML_ONLY` | The message has HTML but no plain-text part. | Send a `text` version along with `html`. | | `R_PARTS_DIFFER` | The plain-text and HTML parts say very different things. | Generate the text part from the same content as the HTML. | | `HTML_SHORT_LINK_IMG_1` | A short HTML body that's mostly a linked image. | Add real text. Don't send image-only emails. | | `SUBJ_ALL_CAPS` | The subject is in capital letters. | Use normal capitalization. | | `PHISHING` | A link's visible text shows a different domain from where it actually goes. | Make the visible URL match the link, or use descriptive link text instead of a URL. | | `ZERO_FONT`, `MANY_INVISIBLE_PARTS` | The message contains text hidden from the reader. | Remove hidden text, such as words set to a font size of 0. | | `DBL_SPAM`, `URIBL_BLACK`, `SEM_URIBL` | A link points to a domain on a blocklist. | Remove the link or replace it with one on your own domain. Avoid public URL shorteners. | | `R_SUSPICIOUS_URL` | A link uses an unusual or obfuscated address, such as a raw IP address. | Link to normal domain names. | | `FREEMAIL_REPLYTO` | The `Reply-To` uses a free mailbox provider such as Gmail. | Use an address on your own domain. | | `MIME_BAD_EXTENSION`, `MIME_DOUBLE_BAD_EXTENSION` | An attachment has a risky file type, such as an executable or a name like `invoice.pdf.exe`. | Don't attach executables. Link to the file instead. | | `BAYES_SPAM` | The wording resembles messages known to be spam. | Rewrite promotional phrasing and reduce sales language. | | `FUZZY_DENIED` | The message closely matches known spam. | Write your own content rather than reusing text from bulk templates. | ## Resend a held message Fix the cause first. A retry sends **exactly the same content** again, so it only helps when the problem was outside the message itself, for example a linked domain that has since been removed from a blocklist. - **If the content triggered the rules,** fix your template or code and send a new message. - **If the cause is resolved,** retry the held message: **Dashboard** Open the email in **Email API → Emails** and select **Retry**. Emailit creates a new email with the same content and leaves the original unchanged. **API** Call [Retry an email](/docs/api-reference/emails/retry/) with the held email's ID. ```bash curl https://api.emailit.com/v2/emails/em_5Vb2Nq8Xc1Jm/retry \ -X POST \ -H "Authorization: Bearer $EMAILIT_API_KEY" ``` The response contains the new email's `id` and the `original_id`. The retry is scored again before it's sent and costs credits like any other email. You can retry held emails for up to 30 days, as long as their content hasn't been removed by your [data retention](/docs/data-retention/) settings. See [Retry and forward](/docs/email-api/retry-and-forward/). > **Note:** Spam scores aren't the only reason for `held`. A message is also held when the workspace runs out of credits, its domain is paused, the workspace is suspended or Emailit has put the API key on hold. The email's delivery history shows the reason. ## Related - [Best practices](/docs/deliverability/best-practices/): Content habits that keep scores low. - [Email statuses](/docs/logs/email-statuses/): What held and every other status means. --- Source: https://emailit.com/docs/deliverability/spam-checks/ --- # Warm up a domain or IP > Build sending reputation on a new domain or dedicated IP by increasing volume gradually, with an example ramp schedule and how Emailit's sending limits fit in. Mailbox providers don't trust a sender they've never seen. If a new domain or IP suddenly sends thousands of messages, much of it lands in spam or gets deferred. Warming up means growing your volume step by step so providers can learn that your mail is wanted. This guide shows when to warm up, an example schedule and how Emailit's limits fit in. ## When to warm up - **A new sending domain,** including a new subdomain such as `news.acme.com`. Domain reputation matters even on Emailit's shared IPs. - **A new dedicated IP.** It has no history at all. - **A domain that's been quiet** for a month or more. - **A big jump in volume,** for example moving all your mail to Emailit at once, or a first campaign to a list you haven't mailed in a long time. If you're migrating from another provider with an established domain, you can usually move faster, but still spread the first large sends over several days. ## Start with your best recipients Send your first days of mail to the people most likely to open it: - transactional mail such as receipts and password resets, which people expect, - subscribers who opened or clicked in the last 30 to 90 days, - recent sign-ups. Leave older or unverified addresses for later, once the domain has a track record. Verify them first with [email verification](/docs/email-verification/lists/). ## Example ramp schedule This schedule takes a new domain to about 50,000 emails a day in three weeks. Adjust it to your target volume. Only move to the next step when the previous day's numbers look healthy. | Day | Emails per day | | --- | --- | | 1 | 200 | | 2 | 500 | | 3 | 1,000 | | 4 | 2,000 | | 5 | 3,000 | | 6 to 7 | 5,000 | | 8 to 10 | 10,000 | | 11 to 14 | 20,000 | | 15 to 17 | 35,000 | | 18 to 21 | 50,000 | Campaigns send to a whole audience at once. To follow a schedule with campaigns, split your list into several smaller [audiences](/docs/audiences/) and send to one more each day, starting with the most engaged. ## Watch these numbers Check them every day while you ramp up: | Metric | Healthy | Where to see it | | --- | --- | --- | | Bounce rate | Under 2% | **Dashboard** sending health, [Analytics](/docs/analytics/) | | Spam complaint rate | Under 0.1% | [Analytics](/docs/analytics/), [Google Postmaster Tools](https://postmaster.google.com) | | Deferrals | Few `attempted` emails with `421` or `451` replies | **Email API → Emails**, filtered by status `attempted` | | Opens and clicks | Steady or rising | [Analytics](/docs/analytics/) | If bounces, complaints or deferrals rise, hold your volume at the current level, or step back one level, until they settle. Pushing through makes it worse. ## How Emailit's limits fit in Sending limits cap how fast your workspace can send. They aren't a warm-up plan, but they keep a new workspace from sending too much too soon. - **Defaults.** New workspaces can send 2 emails per second and 5,000 emails per day, shared between the API and SMTP. Daily limits reset at midnight UTC. Over the limit, the API returns `429` and SMTP replies `452`. - **Pro and Business** get automatic raises, at most once every 7 days, while your bounce rate stays under 2%. Each raise moves the daily limit one step, for example from 5,000 to 10,000 to 25,000. See [Sending health](/docs/deliverability/sending-health/#automatic-limit-raises). - **Pay as you go,** or anyone who needs more right away, can request a higher limit from the **Sending Limits** card on the **Dashboard** with **Request Increase**. Plan your ramp so each day stays inside your limits. See [Limits](/docs/limits/) for the full list. ## Shared and dedicated IPs **On shared IPs**, the IPs are already warm, because many senders use them. You only need to warm up your domain, and you can usually follow a faster version of the schedule above. **On a dedicated IP**, both the IP and the domain are new. Emailit warms up new dedicated IPs automatically by capping their daily volume: the cap starts at 500 emails, and each day it's reset to about 1.5 times what the IP sent the day before. You don't configure this, but plan for a slower ramp, typically two to six weeks depending on your target volume. See [Dedicated IPs](/docs/deliverability/dedicated-ips/). ## Related - [Best practices](/docs/deliverability/best-practices/): Authentication, consent and list hygiene. - [Sending health](/docs/deliverability/sending-health/): How bounce rates affect your score and limits. --- Source: https://emailit.com/docs/deliverability/warm-up/ --- # API keys > Create Full Access and Sending Only API keys, restrict them to a domain, use them for SMTP, and rotate them without downtime. API keys authenticate your requests to the REST API, your SMTP connections and API-key sessions on the MCP server. This page explains the two key scopes, how to create and manage keys, and how to store and rotate them safely. ## How API keys work - Each key belongs to one workspace. Everything you do with it happens in that workspace. - New keys start with `secret_` followed by 32 letters and digits, for example `secret_••••••••`. Keys created before the prefix was introduced keep working. - Emailit shows the full key once, when you create or regenerate it. Copy it then; you can't view it again. - You send the key as a bearer token: `Authorization: Bearer secret_…`. For SMTP, the key is the password. ## Scopes Every key has one of two scopes. You choose the scope when you create the key. | | Full Access (`full`) | Sending Only (`sending`) | | --- | --- | --- | | Send email (`POST /emails`) | Yes | Yes | | Reschedule, cancel, retry and forward an email | Yes | Yes | | SMTP relay | Yes | Yes | | Read emails (list, retrieve, raw, body, metadata, attachments) | Yes | No | | Domains, templates, contacts, audiences, suppressions, webhooks, events, campaigns, automations, verification and API keys | Yes | No | | MCP tools | All tools | `send-email`, `update-email`, `cancel-email`, `retry-email`, `forward-email` and `get-current-workspace` | | Can be restricted to one sending domain | No | Yes | A Sending Only key that calls any other endpoint gets `403` with a message such as `Permission denied: read`. Use Full Access keys for back-office jobs that manage resources, and Sending Only keys for anything that only needs to send. ### Restrict a key to one domain When you create a Sending Only key you can pick one verified sending domain. The key can then only send from addresses on that domain: - Over the API, sending from another domain returns `403` with `"error": "Domain not authorized"`. - Over SMTP, the message is rejected after `DATA` with `530 API key is restricted to sending domain: …`. Restricted keys are a good fit for per-app or per-customer credentials, and for keys you have to hand to third-party software such as a CMS plugin. ## Before you begin - You need the **Admin** role in the workspace to create, edit, regenerate or delete keys. Members can see the list of keys but not change it. See [Members and roles](/docs/workspaces/members-and-roles/). - To send with a key, you need at least one [verified sending domain](/docs/domains/add-a-domain/). ## Create an API key **Dashboard** 1. **Open API keys.** Go to **Email API → API Keys** and select **Add API key**. 2. **Name the key.** Enter a **Name** that says where the key is used, for example `production-web` or `wordpress-blog`. Names must be unique in the workspace. 3. **Choose a scope.** Under **Scope**, choose **Full Access** or **Sending Only**. 4. **Optionally restrict the domain.** For a Sending Only key, pick a sending domain under **Domain**, or leave it empty to allow every verified domain. 5. **Create and copy the key.** Select **Create**. Copy the key from the dialog and store it in your secrets manager before you close it. Emailit shows the key only once. **API** Call [Create an API key](/docs/api-reference/api-keys/create/) with a Full Access key. `scope` defaults to `full`; `sending_domain_id` only applies to Sending Only keys. ```bash curl https://api.emailit.com/v2/api-keys \ -H "Authorization: Bearer $EMAILIT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "production-web", "scope": "sending", "sending_domain_id": "dom_3xQ7mLp2RkT9vB4nHs8YcWd1Ze6" }' ``` The `201` response is the only one that includes `key`: ```json { "object": "api_key", "id": "key_4F2kN8sQwE1rT6yU3iO9pA7sD5f", "name": "production-web", "scope": "sending", "sending_domain_id": "dom_3xQ7mLp2RkT9vB4nHs8YcWd1Ze6", "last_used_at": null, "created_at": "2026-10-01T09:30:00.000Z", "updated_at": "2026-10-01T09:30:00.000Z", "key": "secret_••••••••••••••••••••••••••••••••" } ``` A name that's already taken returns `409`. ## Use the key Pass the key in the `Authorization` header of every API request: ```bash curl https://api.emailit.com/v2/emails \ -H "Authorization: Bearer $EMAILIT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "from": "Acme ", "to": "ada@example.com", "subject": "Your order has shipped", "text": "Your order #1042 is on its way." }' ``` To send over SMTP, use the key as the password: | Setting | Value | | --- | --- | | Host | `smtp.emailit.com` | | Port | `587` (STARTTLS, recommended), `465` (TLS), `2525` or `2587` (STARTTLS) | | Username | `emailit` | | Password | Your API key | See [SMTP settings](/docs/smtp/settings/) for every option. ## Manage keys Open a key from **Email API → API Keys** to see its scope, domain, **Created** date and **Last used** time, and the SMTP settings to use with it. | Action | What happens | API | | --- | --- | --- | | **Edit** | Renames the key. Only the name is saved; to change the scope or domain restriction, create a new key and rotate to it. | [Update an API key](/docs/api-reference/api-keys/update/) | | **Regenerate** | Issues a new secret for the same key and shows it once. The old secret stops working immediately. The key keeps its ID, name, scope and domain, and **Last used** resets. | [Regenerate an API key](/docs/api-reference/api-keys/regenerate/) | | **Delete** | The key stops working immediately and disappears from the list. You can't undo this. | [Delete an API key](/docs/api-reference/api-keys/delete/) | **Last used** updates whenever the key authenticates an API request or an SMTP login. A key that has never been used shows **Never**. In the API, endpoints that take a key ID also accept the key's name. ## Store keys safely - **Keep keys on the server.** Never put a key in browser JavaScript, a mobile app, a public repository or a support ticket. Anyone with the key can send email as you and spend your credits. - **Use environment variables or a secrets manager.** Load the key at runtime, for example from `EMAILIT_API_KEY`. Add `.env` files to `.gitignore`. - **Give each app and environment its own key.** Separate keys for production, staging and each third-party tool make it easy to see who sent what and to revoke one without touching the others. - **Use the narrowest scope.** If an app only sends email, give it a Sending Only key, restricted to its domain where possible. - **Watch usage.** **Email API → Logs** lists API and SMTP requests per key, and you can filter **Email API → Emails** by API key. See [Request logs](/docs/logs/request-logs/). - **Act fast on a leak.** If a key is exposed, regenerate or delete it right away, then check the logs for unexpected sends. ## Rotate a key without downtime Regenerating a key cuts off the old secret at once, so use it only when a key is compromised. For planned rotation, run the old and new keys side by side: 1. **Create a new key.** Add a key with the same scope and domain restriction as the one you're replacing. Give it a name that shows the date, such as `production-web-2026-10`. 2. **Deploy the new key.** Update the secret in your secrets manager or environment and roll it out to every server, worker and scheduled job that uses the old key. 3. **Confirm the switch.** Open the new key and check that **Last used** is recent. In **Email API → Logs**, filter by the old key and check that requests have stopped. 4. **Delete the old key.** When the old key's **Last used** time no longer changes, delete it. ## Troubleshooting | Symptom | Cause | Fix | | --- | --- | --- | | `401` `Invalid API key` | The key was deleted, regenerated or mistyped. | Copy the current key into your configuration, including the `secret_` prefix. | | `403` `Permission denied: read` or `Permission denied: full` | A Sending Only key called an endpoint outside its scope. | Use a Full Access key for that call. | | `403` `Domain not authorized` | The key is restricted to a different domain than the `from` address. | Send from the key's domain or use another key. | | SMTP `535 Authentication failed` | The password isn't a valid API key. | Use the API key as the password and `emailit` as the username. | ## Related - [Authentication](/docs/api-reference/authentication/): How requests are authenticated. - [API keys API](/docs/api-reference/api-keys/): Create, list, update, regenerate and delete keys. - [SMTP settings](/docs/smtp/settings/): Host, ports, TLS and credentials. - [Security](/docs/security/): How Emailit protects your account and data. --- Source: https://emailit.com/docs/developers/api-keys/ --- # Developer overview > Base URL, authentication, object IDs, errors, pagination, rate limits, SDKs, webhooks and MCP. The conventions every Emailit integration shares. This page collects the conventions you need before you write code against Emailit: where the API lives, how requests are authenticated, how objects are identified, and how errors, pagination and rate limits work. Each section links to the detailed reference. ## Ways to integrate | Interface | Endpoint | Use it for | | --- | --- | --- | | REST API | `https://api.emailit.com/v2` | Sending email and managing every resource from code. | | SMTP relay | `smtp.emailit.com` | Apps, frameworks and CMSs that already speak SMTP. See [SMTP settings](/docs/smtp/settings/). | | Webhooks | Your HTTPS endpoint | Real-time delivery, engagement and resource events. | | MCP server | `https://api.emailit.com/mcp` | Letting AI assistants such as Claude, ChatGPT and Cursor work with your workspace. | | OAuth 2.1 | `https://api.emailit.com/oauth/*` | Integrations that act on behalf of Emailit users without handling their API keys. | Not sure whether to use the API or SMTP? Read [API or SMTP](/docs/get-started/api-or-smtp/). ## Base URL and versioning All REST endpoints live under one base URL: ```text https://api.emailit.com/v2 ``` `v2` is the current and only documented version. The legacy `v1` API is deprecated; see [Versioning](/docs/api-reference/versioning/). ## Authentication Send an API key as a bearer token in the `Authorization` header of every request: ```bash curl https://api.emailit.com/v2/domains \ -H "Authorization: Bearer $EMAILIT_API_KEY" ``` - API keys start with `secret_`. Older keys without the prefix keep working. - Each key belongs to one workspace and has a scope: **Full Access** (`full`) can call every endpoint, **Sending Only** (`sending`) can only send and manage sends. See [API keys](/docs/developers/api-keys/). - OAuth access tokens issued to [OAuth apps](/docs/developers/oauth-apps/) are accepted in the same header. - A missing key returns `401` with `API key required`, an unknown key returns `401` with `Invalid API key`, and a suspended workspace returns `403` with `Workspace is suspended`. Never call the API from a browser or mobile app with your key. Keep it on your server. Details: [Authentication](/docs/api-reference/authentication/). ## IDs and prefixes Every object has a string ID with a type prefix, so you can tell what an ID refers to at a glance. | Object | Prefix | Example | | --- | --- | --- | | Email | `em_` | `em_4K6oASS7KP9ztzWmSN9ndEu13HW` | | Sending domain | `dom_` | `dom_3xQ7mLp2RkT9vB4nHs8YcWd1Ze6` | | API key | `key_` | `key_4F2kN8sQwE1rT6yU3iO9pA7sD5f` | | Audience | `aud_` | `aud_3zR8tY2uI6oP4aS1dF9gH7jK5lM` | | Subscriber | `sub_` | `sub_4K6oASS7KP9ztzWnqS4svxApJzO` | | Contact | `con_` | `con_4A9sD2fG6hJ1kL8zX3cV7bN5mQw` | | Template | `tem_` | `tem_3wE8rT1yU5iO9pA2sD6fG4hJ7kL` | | Suppression | `sup_` | `sup_4K6oASS7KP9ztzWol5ElicOeKFE` | | Webhook | `wh_` | `wh_3mN7bV2cX6zL9kJ4hG1fD8sA5pO` | | Webhook request | `whr_` | `whr_4K6oASS7KP9ztzWpVUIec9Jneax` | | Event | `evt_` | `evt_4K6oASS7KP9ztzWpqId2iIptac5` | | Campaign | `cmp_` | `cmp_4B1nM5qW9eR3tY7uI2oP6aS8dF4` | | Form | `frm_` | `frm_4C3vB7nM1qW5eR9tY2uI6oP8aS4` | | Form submission | `fsub_` | `fsub_4K6oASS7KP9ztzWqvxLV3IsFRs8` | | Automation | `aut_` | `aut_4D5fG9hJ3kL7zX1cV6bN2mQ8wE4` | | Automation run | `aur_` | `aur_4K6oASS7KP9ztzWrWOjGgqompRo` | | Email verification | `ev_` | `ev_4E7gH1jK5lZ9xC3vB8nM2qW6eR4` | | Verification list | `evl_` | `evl_4F9hJ3kL7zX1cV5bN9mQ2wE6rT8` | | DMARC report | `dmr_` | `dmr_4K6oASS7KP9ztzWsW7e5qkOYHO6` | Domains created before the switch to `dom_` IDs can still have `sd_` or `sed_` IDs. Some endpoints also accept a human-readable identifier in place of the ID: a name for API keys, domains, webhooks, campaigns and audiences, and an email address for contacts and suppressions. Emails, templates and events are looked up by ID only. ## Requests and responses - **JSON in, JSON out.** Send request bodies as JSON with `Content-Type: application/json`. Malformed JSON returns `400` with `Invalid JSON in request body`. A request body can be up to 50 MB; an email's final MIME message can be up to 40 MB. - **Errors.** Most errors return `{"statusCode", "error", "message"}`. Validation errors add a `details` array, and sending errors return `validation_errors`. Plan-gated features return `403` with `"error": "plan_required"`. See [Errors](/docs/api-reference/errors/). - **Pagination.** List endpoints take `page` and `limit` (1 to 100) and return `data`, `next_page_url` and `previous_page_url`. Templates and automations use `page` and `per_page`. See [Pagination](/docs/api-reference/pagination/). - **Filtering and sorting.** Filter with `field.condition=value`, combine filters with `match=all` or `match=or`, and sort with `order` and `direction`. For example, `GET /v2/emails?status.exact=bounced&order=created_at&direction=desc`. See [Filtering](/docs/api-reference/filtering/). - **Idempotency.** Send an `Idempotency-Key` header on `POST /emails` and `POST /emails/:id/forward` to make retries safe. Emailit replays the first response for 24 hours. See [Idempotency](/docs/api-reference/idempotency/). - **Rate limits.** Sending is limited per workspace, by default to 2 emails per second and 5,000 emails per day, shared between the API and SMTP. Responses include `ratelimit-*` headers, and a `429` includes `retry-after`. See [Rate limits](/docs/api-reference/rate-limits/) and [Limits](/docs/limits/). ## SDKs Official libraries wrap the REST API for Node.js, PHP, Laravel, Python, Ruby, Go, Java, .NET and Rust. They all live on [GitHub](https://github.com/emailit). See [SDKs and libraries](/docs/sdks/) for install commands, and the framework guides for complete examples, starting with [Node.js](/docs/frameworks/nodejs/). ## Webhooks Webhooks push events to your endpoint as they happen: deliveries, bounces, opens, clicks, inbound mail, and changes to domains, contacts and other resources. Each request carries a JSON array of up to 100 events and is signed with HMAC-SHA256 in the `X-Emailit-Signature` header. Failed requests are retried for up to 11 attempts. Start with [Set up a webhook](/docs/webhooks/set-up/) and [Request signature](/docs/webhooks/request-signature/). ## MCP server and AI tools The hosted [MCP server](/docs/mcp/) at `https://api.emailit.com/mcp` gives AI assistants 109 tools covering the full API v2, from sending email to campaigns and automations. Assistants sign in with OAuth or an API key, and the [Emailit plugins](/docs/mcp/plugins-and-skills/) add skills for ChatGPT, Codex, Claude Code, Cursor and Grok. The docs are also published for AI: every page has a Markdown version, and [/docs/llms.txt](/docs/developers/llms-txt/) indexes them all. ## Next steps - [Create an API key](/docs/developers/api-keys/): Choose a scope, restrict it to a domain and store it safely. - [Send your first email](/docs/quickstart/api/): Make your first API call in a few minutes. - [SDKs and libraries](/docs/sdks/): Official libraries for nine languages and frameworks. - [API reference](/docs/api-reference/): Every endpoint, parameter and response. --- Source: https://emailit.com/docs/developers/ --- # Docs for AI and agents > Give ChatGPT, Claude, Cursor, Codex, Grok and other AI tools accurate Emailit context with llms.txt, Markdown pages, the MCP server and the Emailit plugin. AI assistants give better answers about Emailit when they read the current docs instead of guessing. Emailit gives them three layers of help: | Layer | What it gives the assistant | Start here | | --- | --- | --- | | **Docs as Markdown** | Every docs page and the main pages of emailit.com as plain Markdown, plus llms.txt indexes. | This page | | **MCP server** | 109 tools that act on your workspaces: send email, set up domains, run campaigns and more. | [MCP server](/docs/mcp/) | | **Plugin and skills** | Emailit know-how for Claude Code, Codex, Cursor and Grok Build: setup flows, deliverability checks, SDK patterns and safe handling of keys. | [Plugins and skills](/docs/mcp/plugins-and-skills/) | Use the docs alone to answer questions and write code. Add the MCP server when the assistant should also do the work in your account. ## Files for AI tools | File | What it contains | Use it for | | --- | --- | --- | | [`https://emailit.com/llms.txt`](https://emailit.com/llms.txt) | A short summary of Emailit: products, pricing, credits per action, the MCP server and key links. | Giving an assistant a quick overview of what Emailit is. | | [`https://emailit.com/llms-full.txt`](https://emailit.com/llms-full.txt) | The main pages of emailit.com in one file: products, pricing, credits, programs, answers and comparisons. | Questions about plans, products, partners, referrals and how Emailit compares. | | [`https://emailit.com/docs/llms.txt`](https://emailit.com/docs/llms.txt) | An index of every docs page, grouped by section, with a one-line summary and a link to its Markdown version. It also lists key facts such as the API base URL, SMTP settings and the MCP server URL. | Letting an assistant find and fetch the pages it needs. | | [`https://emailit.com/docs/llms-full.txt`](https://emailit.com/docs/llms-full.txt) | The full text of every docs page in one file. | Loading all of the docs into a project, a knowledge base or an editor's docs index. | These files follow the [llms.txt](https://llmstxt.org) convention and are rebuilt every time the site or the docs change. ## Markdown version of any page Add `.md` to a page's URL, without the trailing slash, to get it as Markdown. This works for the docs and for the main pages of emailit.com: | Page | Markdown | | --- | --- | | `https://emailit.com/docs/quickstart/api/` | `https://emailit.com/docs/quickstart/api.md` | | `https://emailit.com/docs/webhooks/request-signature/` | `https://emailit.com/docs/webhooks/request-signature.md` | | `https://emailit.com/docs/mcp/tools/` | `https://emailit.com/docs/mcp/tools.md` | | `https://emailit.com/pricing/` | `https://emailit.com/pricing.md` | | `https://emailit.com/products/email-api/` | `https://emailit.com/products/email-api.md` | | `https://emailit.com/vs/sendgrid/` | `https://emailit.com/vs/sendgrid.md` | | `https://emailit.com/` | `https://emailit.com/index.md` | Each HTML page points to its Markdown version with ``, so agents and crawlers can find it without guessing the URL. Interactive parts of a page, such as tabs, callouts and plan tables, become plain Markdown, so code samples for every language are included. For the API reference, each resource also has one combined file with all of its endpoints, for example `https://emailit.com/docs/api-reference/emails.md` for the Emails API. ## Page actions Every docs page has a **Copy page** button next to the title. Select it to copy the page as Markdown, ready to paste into a chat. The arrow next to it opens more options: | Action | What it does | | --- | --- | | **Copy as Markdown** | Copies the page's Markdown to your clipboard. | | **View as Markdown** | Opens the `.md` version in a new tab. | | **Open in ChatGPT** | Starts a ChatGPT conversation that asks it to read the page's Markdown, so you can ask questions about it. | | **Open in Claude** | Starts a Claude conversation with the same prompt. | ## Use the docs in your tools - **Chat assistants.** Paste a page with **Copy page**, or ask the assistant to read a `.md` URL. For broad questions, point it at `https://emailit.com/docs/llms.txt` and let it pick pages. - **Projects and knowledge bases.** Add `https://emailit.com/docs/llms-full.txt` as a source in a Claude project, a custom GPT or your team's internal assistant. Refresh it from time to time to pick up changes. - **Code editors.** Add `https://emailit.com/docs/llms-full.txt` (or `https://emailit.com/docs/`) as a documentation source in Cursor or a similar editor, then reference it when you ask for Emailit code. - **Coding agents.** Install the [Emailit plugin](/docs/mcp/plugins-and-skills/) in Claude Code, Codex, Cursor or Grok Build. Its skills load Emailit setup and SDK guidance only when a task needs it, and point the agent at these Markdown pages for details. Prefer a single page's `.md` file for focused questions: it's smaller than the full file and keeps the assistant on topic. ## Combine the docs with the MCP server The docs tell an assistant how Emailit works; the [MCP server](/docs/mcp/) lets it act on your workspace. Used together, an assistant can read the rules and then do the work. For example, in Cursor, Codex or Claude Code with the Emailit MCP server connected: ```text Read https://emailit.com/docs/webhooks/request-signature.md and https://emailit.com/docs/webhooks/event-types.md. Then create an Emailit webhook named "Bounces" that sends email.bounced and email.complained events to https://acme.com/webhooks/emailit, and add an Express route at /webhooks/emailit that verifies the signature with the webhook's secret and marks bounced addresses in our users table. ``` The assistant reads the signing rules from the docs, calls `create-webhook` to set up the endpoint and get the signing secret, and writes code that matches. ## Tips - **Check the date.** Each page shows when it was last updated. If an assistant's answer conflicts with a page, trust the page. - **Keep secrets out of prompts.** The docs never need your API key. Prefer OAuth for the MCP server, and keep keys in your environment, not in chat messages or committed files. - **Report gaps.** If an assistant can't find something in the docs, it's probably missing for people too. Tell us at support@emailit.com. ## For crawlers and tool builders - `robots.txt` allows search engines and AI crawlers, including GPTBot, OAI-SearchBot, ChatGPT-User, ClaudeBot, Claude-User, PerplexityBot and Google-Extended. Only the dashboard API and admin paths are excluded. - Markdown responses use `text/markdown`, and the llms.txt files use `text/plain`. All of them are static files, cached at the edge. - Pages carry schema.org structured data (`TechArticle` for docs, plus breadcrumbs), a canonical URL and an updated date. - The [MCP server](/docs/mcp/) describes every tool with a JSON schema, a title and annotations such as read-only and destructive, and returns structured results. See the [tool reference](/docs/mcp/tools/). ## Related - [MCP server](/docs/mcp/): Let assistants act on your workspace. - [Developer overview](/docs/developers/): The conventions every integration shares. - [Plugins and skills](/docs/mcp/plugins-and-skills/): Emailit skills for coding agents. - [API reference](/docs/api-reference/): Every endpoint in one place. --- Source: https://emailit.com/docs/developers/llms-txt/ --- # Build an OAuth app > Let users connect your app to their Emailit workspace with OAuth 2.1 and PKCE, including client registration, token refresh, revocation and errors. Emailit runs an OAuth 2.1 authorization server at `https://api.emailit.com`. Use it when you build an integration, such as a CRM, a no-code tool or an AI client, that acts on behalf of Emailit users. Your users sign in and approve access in the browser, and you get tokens for the workspaces they choose without ever handling their API keys. This is the same flow the [MCP server](/docs/mcp/) uses. ## How it works 1. **Register a client** with dynamic client registration, or host a client ID metadata document. 2. **Send the user to the authorization URL** with a PKCE code challenge. 3. **The user signs in to Emailit**, chooses the workspaces your app can use and approves the requested scope. 4. **Emailit redirects back** to your redirect URI with a single-use authorization code. 5. **Exchange the code** for a 15-minute access token and a refresh token. 6. **Call the API** with the access token, and refresh it when it expires. ## Endpoints | Method | Path | Purpose | | --- | --- | --- | | `GET` | `/.well-known/oauth-authorization-server` | Authorization server metadata (RFC 8414) | | `GET` | `/.well-known/oauth-protected-resource` | Protected resource metadata for the REST API | | `GET` | `/.well-known/oauth-protected-resource/mcp` | Protected resource metadata for the MCP server | | `POST` | `/oauth/register` | Dynamic client registration (RFC 7591) | | `GET` | `/oauth/authorize` | Browser sign-in and consent | | `POST` | `/oauth/token` | Exchange a code or refresh token | | `POST` | `/oauth/revoke` | Revoke a grant with its refresh token (RFC 7009) | | `GET` | `/oauth/grants` | List grants (the signed-in user's, or one workspace's with an API key) | | `POST` | `/oauth/grants/:id/revoke` | Revoke a grant | | `PUT` | `/oauth/grants/:id/workspaces` | Change which workspaces a grant can use (the user who connected the app) | All paths are on `https://api.emailit.com`. ## Discover the server ```bash curl https://api.emailit.com/.well-known/oauth-authorization-server ``` ```json { "issuer": "https://api.emailit.com", "authorization_endpoint": "https://api.emailit.com/oauth/authorize", "token_endpoint": "https://api.emailit.com/oauth/token", "registration_endpoint": "https://api.emailit.com/oauth/register", "revocation_endpoint": "https://api.emailit.com/oauth/revoke", "jwks_uri": "https://api.emailit.com/.well-known/jwks.json", "code_challenge_methods_supported": ["S256"], "grant_types_supported": ["authorization_code", "refresh_token"], "response_types_supported": ["code"], "token_endpoint_auth_methods_supported": ["none", "client_secret_basic", "client_secret_post"], "client_id_metadata_document_supported": true, "authorization_response_iss_parameter_supported": true, "scopes_supported": ["sending", "full"] } ``` MCP clients start from the protected resource metadata instead. An unauthenticated request to `https://api.emailit.com/mcp` returns `401` with `WWW-Authenticate: Bearer resource_metadata="https://api.emailit.com/.well-known/oauth-protected-resource/mcp"`, and that document points to the authorization server: ```json { "resource": "https://api.emailit.com/mcp", "authorization_servers": ["https://api.emailit.com"], "bearer_methods_supported": ["header"], "scopes_supported": ["sending", "full"] } ``` ## Scopes | Scope | Grants | | --- | --- | | `sending` | Sending and forwarding email, and rescheduling, canceling and retrying sends. The same access as a Sending Only API key. | | `full` | Every REST API endpoint and MCP tool. The same access as a Full Access API key. | `full` already includes everything `sending` allows. Request `sending` if your app only sends and `full` otherwise; a space-separated `sending full` is accepted too. If you leave `scope` out, Emailit uses every scope the client registered. ## Register your client You can register a client in one of two ways. Both work with every flow on this page. ### Dynamic client registration Send a registration request. No authentication is needed, and each IP address can register up to 20 clients per hour. ```bash curl https://api.emailit.com/oauth/register \ -H "Content-Type: application/json" \ -d '{ "client_name": "Acme CRM", "redirect_uris": ["https://crm.acme.com/oauth/emailit/callback"], "grant_types": ["authorization_code", "refresh_token"], "response_types": ["code"], "scope": "full", "token_endpoint_auth_method": "client_secret_basic", "client_uri": "https://crm.acme.com", "logo_uri": "https://crm.acme.com/logo.png" }' ``` | Field | Required | Description | | --- | --- | --- | | `client_name` | Yes | Shown on the consent page. Up to 200 characters. | | `redirect_uris` | Yes | 1 to 10 redirect URIs. See [Redirect URI rules](#redirect-uri-rules). | | `grant_types` | No | Must include `authorization_code`; may include `refresh_token`. Defaults to both. | | `response_types` | No | Only `code`. | | `scope` | No | Space-separated scopes. Defaults to `sending full`. | | `token_endpoint_auth_method` | No | `none` (default) for public clients such as desktop, mobile and browser apps; `client_secret_basic` or `client_secret_post` for server-side apps. | | `client_uri` | No | Your app's homepage. | | `logo_uri` | No | Logo shown on the consent page. | The `201` response echoes the metadata and adds a `client_id`. Confidential clients also get a `client_secret`, shown only once: ```json { "client_id": "3e8a1f52-7c4b-4d29-8e6f-1a5b9c0d2e73", "client_id_issued_at": 1790847000, "client_name": "Acme CRM", "redirect_uris": ["https://crm.acme.com/oauth/emailit/callback"], "grant_types": ["authorization_code", "refresh_token"], "response_types": ["code"], "scope": "full", "token_endpoint_auth_method": "client_secret_basic", "client_uri": "https://crm.acme.com", "logo_uri": "https://crm.acme.com/logo.png", "client_secret": "Ky7WI2z5HxoT6q1z19tuiIev_U1zX0_FKhIIfh0OBco", "client_secret_expires_at": 0 } ``` `client_secret_expires_at` is always `0`: secrets don't expire. Every client, public or confidential, must use PKCE. ### Client ID metadata document If your app can host a static JSON file, you can skip registration. Publish a document at an HTTPS URL and use that URL as your `client_id`: ```json title="https://crm.acme.com/oauth/client.json" { "client_id": "https://crm.acme.com/oauth/client.json", "client_name": "Acme CRM", "client_uri": "https://crm.acme.com", "logo_uri": "https://crm.acme.com/logo.png", "redirect_uris": ["https://crm.acme.com/oauth/emailit/callback"], "grant_types": ["authorization_code", "refresh_token"], "token_endpoint_auth_method": "none", "scope": "full" } ``` - `client_id` in the document must equal the document's URL exactly. - `token_endpoint_auth_method` must be `none`. These are public clients that rely on PKCE. - HTTPS redirect URIs must be on the same host as the document. - Emailit fetches the document during authorization, with a 5-second timeout and without following redirects, and caches it for 5 minutes. - The consent page shows the document's host, for example `crm.acme.com`, instead of `client_name`. AI clients use this method: ChatGPT, Claude and Grok identify themselves with their own client ID metadata documents, so they connect without registering first. For Grok, Emailit also accepts redirects to `console.x.ai`. ### Redirect URI rules - `https://` URIs are allowed. - `http://` is only allowed for loopback hosts: `127.0.0.1`, `localhost` and `[::1]`. For loopback URIs the port may differ at authorization time, but the host, path and query must match. `localhost` and `127.0.0.1` are different hosts. - Private-use schemes such as `cursor://`, `vscode://` or `com.acme.crm://` are allowed for native apps. - `file`, `ftp`, `data`, `javascript`, `blob`, `about` and `vbscript` URIs, and any URI with a `#fragment`, are rejected. - Apart from loopback ports, the `redirect_uri` you send must exactly match a registered one. ## Send the user to Emailit Create a PKCE verifier and challenge, and a random `state`, for each authorization: ```javascript const codeVerifier = randomBytes(32).toString('base64url'); const codeChallenge = createHash('sha256').update(codeVerifier).digest('base64url'); const state = randomBytes(16).toString('base64url'); // Store codeVerifier and state in the user's session. ``` Then redirect the user's browser to the authorization endpoint: ```text https://api.emailit.com/oauth/authorize ?response_type=code &client_id=3e8a1f52-7c4b-4d29-8e6f-1a5b9c0d2e73 &redirect_uri=https%3A%2F%2Fcrm.acme.com%2Foauth%2Femailit%2Fcallback &scope=full &state=Jq3k9V0n2xR7bLm1 &code_challenge=zsrvXVr2pbLDHbccAx_NY9osQ6nwqTnDZlIeFWKQOsg &code_challenge_method=S256 &resource=https%3A%2F%2Fapi.emailit.com ``` | Parameter | Required | Description | | --- | --- | --- | | `response_type` | Yes | `code` | | `client_id` | Yes | Your client ID, or your metadata document URL. | | `redirect_uri` | Yes | One of your registered redirect URIs. | | `code_challenge` | Yes | Base64url-encoded SHA-256 of the code verifier. | | `code_challenge_method` | Recommended | `S256`. `plain` isn't supported. | | `scope` | Recommended | `sending` or `full`. Must be a scope the client registered. | | `state` | Recommended | A random value you check on the callback. | | `resource` | No | The resource you want to call (RFC 8707): `https://api.emailit.com` for the REST API or `https://api.emailit.com/mcp` for MCP. Tokens work on both either way. | Open this URL in the user's browser. Don't fetch it from your backend. ## What the user sees 1. **Sign in to Emailit.** The user enters their email and password. If they use an authenticator app for two-factor authentication, they enter its code or a recovery code next. 2. **Choose workspaces.** The page shows your logo, your client name (or your metadata document's host) and the scopes you requested. The user picks **All my workspaces**, which includes workspaces they create or join later, or **Only these workspaces** with the ones they tick, and chooses where your app starts. 3. **Allow access.** They select **Allow access** or **Deny**. One grant can cover several workspaces. The user can change the list later under **Account > Connected apps** in the dashboard, and your app sees the change on its next request. On approval, Emailit redirects to your redirect URI: ```text https://crm.acme.com/oauth/emailit/callback?code=XvRKyG5Pkplrd3xkRNaEayKkpkebEMdF&state=Jq3k9V0n2xR7bLm1&iss=https%3A%2F%2Fapi.emailit.com ``` Check that `state` matches the value you stored and that `iss` is `https://api.emailit.com`. The code is valid for 10 minutes and can be used once. If the user selects **Deny**, the redirect carries `error=access_denied` instead. ## Exchange the code for tokens Post to the token endpoint as `application/x-www-form-urlencoded` (JSON is also accepted). Send the same `redirect_uri` and the original code verifier: **Confidential client** ```bash curl https://api.emailit.com/oauth/token \ -u "$CLIENT_ID:$CLIENT_SECRET" \ -d grant_type=authorization_code \ -d code="$CODE" \ -d redirect_uri=https://crm.acme.com/oauth/emailit/callback \ -d code_verifier="$CODE_VERIFIER" ``` With `client_secret_basic`, send the client ID and secret in the `Authorization: Basic` header. With `client_secret_post`, send `client_id` and `client_secret` as form fields instead. Don't send the secret both ways. **Public client** ```bash curl https://api.emailit.com/oauth/token \ -d grant_type=authorization_code \ -d client_id="$CLIENT_ID" \ -d code="$CODE" \ -d redirect_uri=http://127.0.0.1:53682/callback \ -d code_verifier="$CODE_VERIFIER" ``` Public clients send `client_id` and no secret. PKCE proves that the same client started the flow. ```json { "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Im9hdXRoLWhzMjU2In0…", "token_type": "Bearer", "expires_in": 900, "refresh_token": "XvRKyG5Pkplrd3xkRNaEayKkpkebEMdFn0h1VIuqixAOXybMtJfK8w", "scope": "full" } ``` The access token lasts 15 minutes (`expires_in` is in seconds). Treat it as an opaque string: don't parse it or rely on its contents. ## Call the API Use the access token as a bearer token on the REST API and the MCP server, exactly like an API key: ```bash curl https://api.emailit.com/v2/domains \ -H "Authorization: Bearer $ACCESS_TOKEN" ``` REST requests run in the grant's default workspace: the one the user chose to start in, or changed to later. MCP tools can also target another allowed workspace with their `workspace` argument. Every request uses the granted scope and the user's role in the workspace, so a request outside the scope, or an Admin-only route called by a workspace Member, returns `403`. If the user leaves a workspace, the grant loses it too. ## Refresh tokens Before the access token expires, or when a request returns `401`, get a new one: ```bash curl https://api.emailit.com/oauth/token \ -u "$CLIENT_ID:$CLIENT_SECRET" \ -d grant_type=refresh_token \ -d refresh_token="$REFRESH_TOKEN" ``` - **Rotation.** Every refresh returns a new `refresh_token` that's valid for 60 days. Store it and discard the old one. As long as you refresh within 60 days, the connection doesn't expire. - **Reuse detection.** If an old refresh token is used again more than a minute after it was replaced, Emailit returns `invalid_grant` (`Refresh token reuse detected`) and revokes the whole grant. The user then has to authorize again. Serialize refreshes so two workers never use the same refresh token. - **Narrower scope.** You can pass `scope` to get an access token with fewer scopes than the grant. You can't ask for more. ## Revoke a grant When a user disconnects Emailit from your app, revoke the grant with its refresh token: ```bash curl https://api.emailit.com/oauth/revoke \ -u "$CLIENT_ID:$CLIENT_SECRET" \ -d token="$REFRESH_TOKEN" ``` The endpoint authenticates your client the same way as the token endpoint and always returns `200` with an empty body. Revoking the refresh token revokes the whole grant: its access tokens stop working on the next request. Access tokens can't be revoked on their own; `token_type_hint=access_token` returns `invalid_request`. Users can also revoke your app's access from their side under [Connected apps](/docs/account/connected-apps/) in the dashboard, or with the grants API below. ## Manage grants The grants API lists and changes grants from the user's side. It accepts the user's dashboard session or a Full Access API key: | Caller | `GET /oauth/grants` | `POST /oauth/grants/:id/revoke` | `PUT /oauth/grants/:id/workspaces` | | --- | --- | --- | --- | | The user who connected the app | Their grants across all workspaces | Revokes the whole grant | Changes the grant's workspaces | | Full Access API key | Grants that include the key's workspace | Removes the key's workspace from the grant; the grant is revoked when no workspace is left | Not allowed (`403`) | Each grant in the list has this shape: ```json { "id": "9b1f6c2e-4d3a-4f8e-9a7b-2c5d8e1f0a3b", "client_id": "https://chatgpt.com/oauth/client.json", "client": { "name": "ChatGPT", "uri": "https://chatgpt.com", "logo_uri": null }, "default_workspace": { "id": "w3f9a1c2e", "name": "Acme" }, "workspace_access": "selected", "workspaces": [{ "id": "w3f9a1c2e", "name": "Acme" }], "scopes": ["sending", "full"], "resource": "https://api.emailit.com/mcp", "created_at": "2026-10-01T09:30:12Z", "revoked_at": null, "revoked_reason": null } ``` To change a grant's workspaces, send `access` (`all` or `selected`), `workspace_ids` for `selected`, and an optional `default_workspace_id` that must be one of the allowed workspaces: ```bash curl -X PUT https://api.emailit.com/oauth/grants/9b1f6c2e-4d3a-4f8e-9a7b-2c5d8e1f0a3b/workspaces \ -H "Authorization: Bearer $DASHBOARD_SESSION" \ -H "Content-Type: application/json" \ -d '{ "access": "selected", "workspace_ids": ["w3f9a1c2e"], "default_workspace_id": "w3f9a1c2e" }' ``` The user must be a member of every workspace they select. A revoked grant can't be edited (`409`); the app has to connect again. ## Errors OAuth endpoints return errors in the OAuth format, not the [REST API error format](/docs/api-reference/errors/): ```json { "error": "invalid_grant", "error_description": "Authorization code has expired" } ``` | Error | Status | When | | --- | --- | --- | | `invalid_request` | 400 | A parameter is missing or invalid, the client is unknown at authorization, the `redirect_uri` isn't registered, `code_challenge_method` isn't `S256`, or a client secret was sent in both the header and the body. | | `invalid_client` | 401 | The token or revocation endpoint can't authenticate the client: unknown client, missing or wrong secret. | | `invalid_grant` | 400 | The code is invalid, used or expired; the `redirect_uri` or PKCE verifier doesn't match; or the refresh token is invalid, expired, revoked or reused. | | `invalid_scope` | 400 | The scope isn't supported, wasn't registered for the client, or exceeds the grant on refresh. | | `unsupported_response_type` | 400 | `response_type` isn't `code`. | | `unsupported_grant_type` | 400 | `grant_type` isn't `authorization_code` or `refresh_token`. | | `access_denied` | Redirect | The user selected **Deny**. | | `too_many_requests` | 429 | More than 20 registrations per hour from one IP address. | | `server_error` | 500 | Something went wrong on Emailit's side. Try again. | Until the `client_id` and `redirect_uri` are validated, authorization errors are shown as JSON in the browser and never redirected. After that, errors are redirected to your callback (with `error`, `error_description`, `state` and `iss`) only for loopback and private-use redirect URIs and for metadata-document clients; other clients get a JSON error page. A user's **Deny** is always redirected. ## Security checklist - Keep `client_secret` and refresh tokens on your server, encrypted at rest. Never ship a client secret in a mobile, desktop or browser app; use a public client with PKCE instead. - Generate a new `state` and code verifier for every authorization, and check `state` and `iss` on the callback. - Register exact redirect URIs. Don't use open redirectors as callbacks. - Store each grant with the workspace it belongs to, and handle `invalid_grant` by asking the user to connect again. - Request `sending` if you only send email. ## Related - [Workspaces and permissions](/docs/mcp/workspaces-and-permissions/): Grants, token lifetimes and revocation. - [API reference](/docs/api-reference/): The endpoints your tokens can call. - [API keys](/docs/developers/api-keys/): For your own scripts, keys are simpler. - [MCP server](/docs/mcp/): The OAuth flow in practice. --- Source: https://emailit.com/docs/developers/oauth-apps/ --- # Bounce categories > Hard and soft bounces grouped by cause, with typical SMTP codes, how Emailit classifies each one, its effect on suppressions and what to do about it. A bounce is a message that couldn't be delivered. This page groups bounces by cause so you can tell quickly whether to remove an address, fix your setup or simply wait. For individual codes, see [SMTP reply codes](/docs/dictionary/smtp-reply-codes/) and [enhanced status codes](/docs/dictionary/enhanced-status-codes/). ## Hard and soft bounces | | Hard bounce | Soft bounce | |---|---|---| | Meaning | Permanent: the message will never be delivered as sent. | Temporary: the message might be delivered later. | | Typical codes | `5xx` replies, `5.x.x` enhanced codes | `4xx` replies, `4.x.x` enhanced codes, timeouts | | Emailit status | `bounced` | `attempted` while Emailit retries | | What Emailit does | Stops trying and may suppress the address. | Retries up to 7 times over about 21 hours, then bounces and suppresses the address. | ## How Emailit classifies a bounce Emailit classifies each failed delivery from the receiving server's reply: 1. **Known permanent replies.** Codes `550`, `551`, `553` and `554`, and replies whose text says the failure is permanent, mark the email `bounced` right away. 2. **Temporary replies treated as permanent.** Common mailbox-full, over-quota, unknown-user, disabled-mailbox and relay-denied replies are treated as hard bounces even when they arrive with a `4xx` code, because retrying them rarely helps. 3. **Everything else is retried.** Other `4xx` and `5xx` replies, timeouts, connection errors and DNS failures mark the email `attempted`. Emailit retries after 10, 20, 40, 80, 160, 320 and 640 minutes. After the seventh failed attempt the email is `bounced` with the reason "Maximum number of delivery attempts (7) has been reached." 4. **Asynchronous bounces.** Some servers accept a message (`250`, status `delivered`) and send a bounce message ([DSN](/docs/glossary/#dsn)) to your return path later. Emailit matches the DSN to the original email and changes its status to `bounced`. Some replies also slow delivery down for a while: a `451` pauses that IP-to-domain route for 5 minutes, and a `550 5.7.1` block that isn't about content pauses it for 1 hour. ## Effect on suppressions If [automatic suppression](/docs/suppressions/manage/) is on for bounces, Emailit adds the address to your [suppression list](/docs/suppressions/) in these cases: | Situation | Suppressed | |---|---| | An asynchronous bounce (DSN) arrives for the email. | Immediately, with reason `bounce`. | | The email fails all 7 delivery attempts. | Immediately, with reason `too many soft fails`. | | A hard bounce, when the same address already hard bounced in the last 24 hours. | On the second hard bounce, with reason `too many bounces`. | | The recipient reports the email as spam. | Immediately, as a `complaint` suppression (if complaints are suppressed). | A successful delivery to an address removes its `recipient` suppression. Pay as you go workspaces always suppress bounces and complaints; on Pro and Business you can change this in **Workspace → Settings** under **Suppressions**. ## Categories In the **Suppression** column, *repeat* means Emailit suppresses the address on its second hard bounce within 24 hours, and *after retries* means it suppresses the address when all 7 attempts fail. An asynchronous bounce suppresses the address right away in every category. | Category | Type | Typical codes | Emailit | Suppression | |---|---|---|---|---| | Mailbox doesn't exist | Hard | `550 5.1.1`, `550 5.1.10`, `553 5.1.3`, `550 5.4.1` | Bounced | Repeat | | Domain doesn't exist | Hard | `550 5.1.2`, `5.4.4`, no MX records | Retried, then bounced | After retries | | Mailbox full | Usually soft | `452 4.2.2`, `552 5.2.2` | Known over-quota replies bounced, others retried | Repeat, or after retries | | Mailbox disabled | Hard | `550 5.2.1`, Gmail `4.2.1` inactive | Bounced | Repeat | | Policy or blocked | Hard | `550 5.7.1`, `554 5.7.1`, `550 5.7.606` | Bounced, with a 1-hour back-off for `550 5.7.1` | Repeat | | Spam or content | Hard | `550 5.7.1`, `554 5.7.0` | Bounced | Repeat | | Reputation | Soft or hard | `421 4.7.0`, `421 4.7.28`, `550 5.7.1` | `4xx` retried, `5xx` bounced | Repeat, or after retries | | Rate limited or greylisted | Soft | `421`, `450 4.7.1`, `451 4.7.1` | Retried, with a 5-minute back-off on `451` | After retries | | Message too large | Hard | `552 5.3.4`, `552 5.2.3` | Retried, then bounced | After retries | | TLS required | Hard | `530 5.7.0` | Retried, then bounced | After retries | | Authentication failure (DMARC) | Hard | `550 5.7.26`, `550 5.7.509`, `550 5.7.515`, `550 5.7.23` | Bounced | Repeat | ### Mailbox doesn't exist The address was never created, was deleted, or has a typo. Example: `550 5.1.1 The email account that you tried to reach does not exist.` **What to do:** Remove the address from your lists. If many addresses from one source bounce this way, the source is the problem, so [verify the list](/docs/email-verification/lists/) before you send to it again. ### Domain doesn't exist The recipient's domain has no MX or A record, often because of a typo such as `gmial.com` or an expired domain. Because DNS failures can be temporary, Emailit retries before it bounces the email. **What to do:** Fix the typo or remove the address. Validate addresses at sign-up so typos don't reach your list. ### Mailbox full The recipient is over their storage quota. Example: `452 4.2.2 The email account that you tried to reach is over quota.` Emailit treats the common over-quota replies as hard bounces so it doesn't keep retrying a mailbox that's unlikely to be emptied. **What to do:** Nothing for one-off cases. An address that stays full is usually abandoned, so remove it. ### Mailbox disabled The account exists but was closed, suspended or marked inactive by the provider. Example: `550 5.2.1 The email account that you tried to reach is disabled.` **What to do:** Remove the address. ### Policy or blocked The receiving server refused the message because of a local policy: a blocklist, a firewall rule, or a recipient that only accepts mail from known senders. The address itself may be fine. Example: `550 5.7.1 Service unavailable, client host blocked using Spamhaus.` **What to do:** Read the message text for the reason and any link. If it names a blocklist, check your recent complaint and bounce rates, then contact [support](mailto:support@emailit.com) with the full reply. See [best practices](/docs/deliverability/best-practices/). ### Spam or content A spam filter rejected the message because of its content, links or attachments. Example: `554 5.7.1 Message rejected as spam.` **What to do:** Check the email's **Spam Checks** panel, remove URL shorteners and links to domains with a poor reputation, balance images with text and send a plain-text part. Emailit also holds messages that score 7 or more before they're sent. See [Spam checks](/docs/deliverability/spam-checks/). ### Reputation The provider distrusts your sending domain or IP because of past complaints, bounces or spam-trap hits. Gmail, for example, defers with `421 4.7.28` when it sees an unusual rate of unsolicited mail. **What to do:** Lower volume to that provider, stop sending to unengaged and unverified addresses, and watch your [sending health](/docs/deliverability/sending-health/). Warm up new domains gradually. See [Warm-up](/docs/deliverability/warm-up/). ### Rate limited or greylisted The receiving server is throttling you or wants an unfamiliar sender to try again later. Example: `451 4.7.1 Greylisting in action, please come back later.` **What to do:** Nothing. Emailit's retries handle greylisting and short throttling. If one provider defers most of your mail for hours, send in smaller batches. ### Message too large The message is over the receiving server's size limit, which is often 25 MB or less even though Emailit accepts up to 40 MB. Encoding attachments adds about a third to their size. **What to do:** Send smaller attachments or link to the files instead. See [Attachments](/docs/email-api/attachments/). ### TLS required The receiving server only accepts encrypted connections. Emailit's MTA upgrades to TLS whenever the receiving server offers `STARTTLS`, so this bounce is rare and usually points to a misconfigured receiving server. **What to do:** Contact [support](mailto:support@emailit.com) with the full reply. ### Authentication failure (DMARC) The message failed SPF and DKIM alignment and the `From` domain's DMARC policy is `quarantine` or `reject`, or the provider requires authentication from all senders. Examples: `550 5.7.26 Unauthenticated email from acme.com is not accepted due to domain's DMARC policy.` and `550 5.7.509 Access denied, sending domain acme.com does not pass DMARC verification.` **What to do:** Check that the sending domain is [verified](/docs/domains/verification/) in Emailit, so DKIM signs with your domain. If another service sends as your domain, fix its SPF and DKIM too, and use [DMARC reports](/docs/dmarc/reports/) to find it. ## Complaints aren't bounces A spam complaint means the email was delivered and the recipient marked it as spam. Emailit receives complaints through [feedback loops](/docs/deliverability/bounces-and-complaints/), sets the status to `complained`, sends an `email.complained` event and suppresses the address as a `complaint`. Treat complaints more seriously than bounces: they hurt your reputation faster. ## Related - [Bounces and complaints](/docs/deliverability/bounces-and-complaints/) - [Email statuses](/docs/logs/email-statuses/) - [Manage suppressions](/docs/suppressions/manage/) - [Sending health](/docs/deliverability/sending-health/) --- Source: https://emailit.com/docs/dictionary/bounce-categories/ --- # DNS records for email > Syntax and examples for MX, TXT, SPF, DKIM, DMARC, CNAME, PTR, BIMI, MTA-STS and TLS-RPT records, plus the exact records Emailit asks you to publish. Email authentication and routing run on DNS. This page explains the record types involved, their syntax and common mistakes. For the step-by-step setup of a sending domain, see [DNS records](/docs/domains/dns-records/). ## Records Emailit asks for When you [add a domain](/docs/domains/add-a-domain/) such as `acme.com`, Emailit shows these records on the domain's **DNS Setup** tab. The DKIM key is unique to your domain. | Purpose | Type | Name | Value | Required | |---|---|---|---|---| | Return path (bounces) | `MX` | `emailit.acme.com` | `feedback-smtp.ffdc-1.emailit.com`, priority `10` | Yes | | SPF for the return path | `TXT` | `emailit.acme.com` | `v=spf1 include:_spf.emailit.com ~all` | Yes | | DKIM | `TXT` | `emailit._domainkey.acme.com` | `v=DKIM1; t=s; h=sha256; p=MIIBIjANBg...` | Yes | | DMARC | `TXT` | `_dmarc.acme.com` | `v=DMARC1; p=none;` | Recommended | | Open and click tracking | `CNAME` | `go.acme.com` | `go.emailitmail.com` | Only for tracking | | Inbound email | `MX` | `inbound.acme.com` | `inbound.emailitmail.com`, priority `10` | Only for inbound | The SPF, DKIM and return-path records must all pass before the domain can send. The tracking and inbound subdomains are configurable, so yours may differ from `go` and `inbound`. With [DMARC reports](/docs/dmarc/set-up/) turned on, the suggested DMARC record also includes `rua` and `ruf` addresses on `dmarc.emailitmail.com`. > **Your root SPF record doesn't change:** Emailit sends with an envelope sender on `emailit.acme.com`, so SPF is checked against that subdomain's record. You don't need to add Emailit to the SPF record on `acme.com`, and the 10-lookup limit of your root record isn't affected. ## MX An MX record names the servers that accept mail for a domain. Each record has a priority, and lower numbers are tried first. ```text acme.com. 3600 IN MX 10 mx1.mailprovider.example. acme.com. 3600 IN MX 20 mx2.mailprovider.example. ``` - Point an MX record to a hostname, never to an IP address or a CNAME. - Emailit's return-path and inbound MX records live on subdomains (`emailit.` and `inbound.`), so they don't affect the mailboxes on your root domain. ## TXT TXT records hold text. SPF, DKIM, DMARC, BIMI, MTA-STS and TLS-RPT are all published as TXT records. A single string in a TXT record can be at most 255 characters, so long values such as a 2048-bit DKIM key are split into several quoted strings that receivers join together: ```text emailit._domainkey.acme.com. IN TXT ( "v=DKIM1; t=s; h=sha256; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA..." "...IDAQAB;" ) ``` Most DNS providers split long values for you. If yours doesn't, split the value yourself rather than truncating it. ## SPF SPF lists the servers allowed to send mail with your domain in the envelope sender (`MAIL FROM`). A domain name can have only one SPF record. ```text v=spf1 ip4:203.0.113.10 include:_spf.example.net ~all ``` | Part | Meaning | |---|---| | `v=spf1` | Version. Must come first. | | `ip4:` / `ip6:` | Allow an IP address or range, such as `ip4:203.0.113.0/24`. | | `a` / `mx` | Allow the IPs of the domain's A or MX records. | | `include:` | Allow everything another domain's SPF record allows. Used for email services, such as `include:_spf.emailit.com`. | | `exists:` | Allow if a constructed hostname resolves. Rarely needed. | | `redirect=` | Use another domain's SPF record instead of this one. | | `-all` | Fail everything not listed (hard fail). | | `~all` | Soft fail everything not listed. Receivers treat it as suspicious, and DMARC decides the outcome. | | `?all` | Neutral, no opinion. | | `+all` | Allow everyone. Never use it. | > **The 10-lookup limit:** Evaluating an SPF record may trigger at most 10 DNS lookups. Each `include`, `a`, `mx`, `ptr`, `exists` and `redirect` counts, including those inside included records, while `ip4`, `ip6` and `all` don't. Above 10, the result is `permerror` and SPF fails. Remove services you no longer use, and replace `a` and `mx` with `ip4` ranges where you can. ## DKIM A DKIM record publishes the public key that receivers use to verify the `DKIM-Signature` header. It lives at `selector._domainkey.domain`, where the selector comes from the signature's `s=` tag. Emailit signs with a 2048-bit RSA key and the selector `emailit`. | Tag | Meaning | Emailit's record | |---|---|---| | `v` | Version, `DKIM1`. | `v=DKIM1` | | `k` | Key type, `rsa` (default) or `ed25519`. | Omitted, so RSA. | | `p` | The public key, base64-encoded. An empty `p=` revokes the key. | Your domain's key. | | `t` | Flags: `y` means testing, `s` means the signing identity can't be a subdomain of `d=`. | `t=s` | | `h` | Hash algorithms the key may be used with. | `h=sha256` | | `s` | Service type, `email` or `*`. | Omitted. | Emailit verifies DKIM by comparing the `v`, `k` and `p` values, so extra whitespace, quoting and tag order don't matter. Each selector is independent, so Emailit's `emailit` selector doesn't conflict with keys from other services on the same domain. ## DMARC A DMARC record tells receivers what to do when a message fails both SPF and DKIM alignment, and where to send reports. It lives at `_dmarc.domain`. ```text v=DMARC1; p=quarantine; rua=mailto:dmarc@acme.com; adkim=r; aspf=r; pct=100 ``` | Tag | Meaning | Default | |---|---|---| | `v` | Version, `DMARC1`. Must come first. | Required | | `p` | Policy for the domain: `none` (monitor only), `quarantine` (send to spam) or `reject`. | Required | | `sp` | Policy for subdomains. | Same as `p` | | `rua` | Where to send aggregate reports, as `mailto:` URIs separated by commas. | None | | `ruf` | Where to send forensic (failure) reports. | None | | `pct` | Percentage of failing mail the policy applies to. | `100` | | `adkim` | DKIM alignment: `r` (relaxed, subdomains match) or `s` (strict, exact match). | `r` | | `aspf` | SPF alignment: `r` or `s`. | `r` | | `fo` | When to send forensic reports: `0` (all checks fail), `1` (any check fails), `d` (DKIM fails) or `s` (SPF fails). | `0` | | `ri` | Requested interval between aggregate reports, in seconds. | `86400` | Emailit mail passes DMARC with relaxed alignment on both checks: DKIM signs with `d=acme.com`, and the envelope sender `emailit.acme.com` is a subdomain of `acme.com`. Emailit marks the DMARC record as valid when it has a `p=` policy of `none`, `quarantine` or `reject`. Start with `p=none`, read your [DMARC reports](/docs/dmarc/reports/), then move to `quarantine` and `reject` once every service that sends as your domain passes. ## CNAME A CNAME record makes one hostname an alias of another. Emailit uses one for your [tracking domain](/docs/tracking/custom-tracking-domain/). ```text go.acme.com. 3600 IN CNAME go.emailitmail.com. ``` - A name with a CNAME can't have any other record, and you can't put a CNAME on the root domain. - In Cloudflare, set the tracking record to **DNS only**. A proxied record hides the CNAME, so verification fails. ## PTR A PTR record maps an IP address back to a hostname (reverse DNS). Receivers check that a sending IP has one and that it resolves forward to the same IP. PTR records belong to whoever owns the IP, so Emailit manages them for its sending IPs and you don't publish one. ## BIMI BIMI shows your logo next to authenticated mail in supporting inboxes. It's a TXT record at `default._bimi.domain`: ```text v=BIMI1; l=https://acme.com/brand/logo.svg; a=https://acme.com/brand/vmc.pem ``` | Tag | Meaning | |---|---| | `v` | Version, `BIMI1`. | | `l` | HTTPS URL of the logo, an SVG in the Tiny Portable/Secure profile. | | `a` | HTTPS URL of a Verified Mark Certificate or Common Mark Certificate, which Gmail and Apple Mail require. | BIMI works only when the domain's DMARC policy is `quarantine` or `reject`, with `pct=100`. ## MTA-STS and TLS-RPT MTA-STS and TLS-RPT protect mail sent **to** your domain, so they matter for domains that receive mail, such as your mailboxes or an inbound subdomain. They don't affect mail you send through Emailit. MTA-STS tells sending servers to require valid TLS when they deliver to your MX hosts. It needs a TXT record and a policy file served over HTTPS: ```text _mta-sts.acme.com. IN TXT "v=STSv1; id=20261001" ``` ```text title="https://mta-sts.acme.com/.well-known/mta-sts.txt" version: STSv1 mode: enforce mx: mx1.mailprovider.example max_age: 604800 ``` TLS-RPT asks sending servers to send daily reports about TLS problems they hit when delivering to you: ```text _smtp._tls.acme.com. IN TXT "v=TLSRPTv1; rua=mailto:tls-reports@acme.com" ``` ## TTL Every record has a TTL (time to live): how many seconds resolvers may cache it. Lower the TTL to 300 a day before you change a record, so the change takes effect quickly, and raise it again afterwards. DNS changes usually appear within minutes but can take up to 48 hours. ## Related - [DNS records for Emailit](/docs/domains/dns-records/) - [Set up DNS with Cloudflare](/docs/domains/cloudflare/) - [Domain verification](/docs/domains/verification/) - [Set up DMARC](/docs/dmarc/set-up/) - [Email headers](/docs/dictionary/email-headers/) --- Source: https://emailit.com/docs/dictionary/dns-records/ --- # Email headers > What the common email headers do, from From and Reply-To to DKIM-Signature and List-Unsubscribe, and which ones Emailit adds, rewrites or removes when it sends. Headers are the `Name: value` lines at the top of every email. They carry the sender, recipients, subject, routing history and authentication results. This page explains the headers you're most likely to inspect or set, and what Emailit does with each one when it sends your mail. ## Envelope and headers An email has two sets of addresses, and they don't have to match: | | Envelope | Headers | |---|---|---| | Sender | `MAIL FROM`, the bounce address. Emailit sets it to your [return path](/docs/domains/dns-records/), an address on `emailit.yourdomain.com`. | `From`, the address recipients see. You set it. | | Recipients | `RCPT TO`, where the server actually delivers. | `To` and `Cc`, which recipients see. `Bcc` is never shown. | Receivers check [SPF](/docs/dictionary/dns-records/#spf) against the envelope sender and [DKIM](/docs/dictionary/dns-records/#dkim) against the signature, then DMARC checks that at least one of those domains aligns with the `From` header. ## How to set headers - **Email API.** Use the `from`, `to`, `cc`, `bcc`, `reply_to` and `subject` fields, and pass any other header in the `headers` object. See [Headers and metadata](/docs/email-api/headers-and-metadata/). - **SMTP.** Emailit sends the MIME message your app submits, with the changes listed below. See [SMTP headers](/docs/smtp/headers/). ## Header reference The **Emailit** column tells you whether Emailit adds, rewrites or removes the header, or leaves it to you. ### Addresses and subject | Header | Purpose | Emailit | |---|---|---| | `From` | The author's address and display name, shown to recipients. | You set it. It must use a verified sending domain of the workspace, matched case-insensitively. Otherwise the API returns `422` and SMTP returns `530`. | | `Sender` | The address that actually sent the message when it differs from `From`, for example an assistant sending for a manager. | Passed through unchanged. Emailit checks only `From`. | | `Reply-To` | Where replies go instead of the `From` address. | You set it. Emailit removes it when it contains the same addresses as `From`, because spam filters penalize that. | | `To` | Primary recipients, as shown to everyone. | You set it. Each recipient gets a separate copy with its own email ID. | | `Cc` | Visible copy recipients. | You set it. Each recipient gets a separate copy. | | `Bcc` | Hidden copy recipients. | Removed from the message before delivery. Bcc recipients still get their copy. | | `Subject` | The subject line. | You set it. Emailit encodes non-ASCII characters (RFC 2047) so accents and non-Latin scripts display correctly. | | `Date` | When the message was written. | Set by Emailit to the time it sends the message. | ### Identification and threading | Header | Purpose | Emailit | |---|---|---| | `Message-ID` | A globally unique ID for the message, in the form ``. | Set by Emailit to an ID on your sending domain, replacing any value you send. The API returns it as `message_id`. | | `In-Reply-To` | The `Message-ID` of the message being replied to. Mail clients use it to thread conversations. | Passed through. Set it with the `headers` field or in your MIME. | | `References` | The chain of `Message-ID` values in a thread. | Passed through. | ### Routing and trace | Header | Purpose | Emailit | |---|---|---| | `Return-Path` | The envelope sender, normally added by the receiving server at final delivery. | Set by Emailit to an address on `emailit.yourdomain.com`, so bounces reach Emailit and SPF is checked against your domain. You can't change it. | | `Received` | One line per server the message passed through, newest first. Useful for tracing delays. | Added by Emailit's API or SMTP relay and by its MTA. The SMTP relay rejects a message with `550 Loop detected` after more than 4 passes through it. | ### MIME structure | Header | Purpose | Emailit | |---|---|---| | `MIME-Version` | Declares the message uses MIME, always `1.0`. | Generated by the API. Passed through over SMTP. | | `Content-Type` | The type of the body or part, such as `text/html`, `multipart/alternative` or `multipart/mixed`, and its character set. | Generated by the API from `html`, `text` and `attachments`. Passed through over SMTP. | | `Content-Transfer-Encoding` | How a part is encoded for transport, such as `quoted-printable`, `base64` or `7bit`. | Generated by the API. Passed through over SMTP. | ### Authentication | Header | Purpose | Emailit | |---|---|---| | `DKIM-Signature` | A cryptographic signature over the body and selected headers, checked against the public key in DNS. | Added by Emailit with `d=` set to your domain and the `emailit` selector, plus a second signature for `emailitmail.com` that mailbox providers use for feedback loops. | | `Authentication-Results` | The receiving server's SPF, DKIM and DMARC verdicts. | Added by the recipient's server, not by Emailit. Read it in the recipient's copy to debug authentication. | | `ARC-Seal`, `ARC-Message-Signature`, `ARC-Authentication-Results` | Authenticated Received Chain: forwarders and mailing lists record the authentication results they saw, so later servers can trust them. | Not added by Emailit. Added by intermediaries that forward your mail. | ### Lists and automated mail | Header | Purpose | Emailit | |---|---|---| | `List-Unsubscribe` | A `mailto:` or `https:` link that lets mailbox providers show an unsubscribe button. | Added to every [campaign](/docs/campaigns/) email, pointing to the hosted unsubscribe page. For API or SMTP mail, add your own with the `headers` field or in your MIME. | | `List-Unsubscribe-Post` | Set to `List-Unsubscribe=One-Click` to allow one-click unsubscribe (RFC 8058). Gmail and Yahoo require it for bulk mail. | Added to every campaign email together with `List-Unsubscribe`. | | `Feedback-ID` | An identifier mailbox providers include in complaint data so senders can group it. | Added by Emailit to every message, in the form `:campaign-id:workspace-id:Emailit`. | | `Precedence` | An older hint such as `bulk` or `list` that some auto-responders respect. | Not added. You can set it yourself. | | `Auto-Submitted` | Marks a message as automatically generated, for example `auto-generated` or `auto-replied`, so other systems don't reply to it. | Not added. Set it on automated replies to prevent mail loops. | ### Emailit headers | Header | Purpose | Emailit | |---|---|---| | `X-Emailit-ID` | The email's internal token. Emailit uses it to match bounce messages to the original email. | Added to every message. Don't remove it if you relay Emailit mail through another system. | | `X-Emailit-Tag` | A legacy header for tagging emails. | Not read by Emailit. It's passed through like any custom header. | | `X-Emailit-Meta` | The email's [metadata](/docs/email-api/headers-and-metadata/), base64-encoded. | Added by the API when you send `meta`. It stays in the delivered message, so don't put secrets in metadata. | | `X-Emailit-Tracking` | The email's open and click tracking settings. | Added by the API when you enable tracking for a send. | > **Emailit doesn't read custom SMTP headers:** Over SMTP, Emailit doesn't interpret `X-Emailit-*` headers. Open and click tracking follow the sending domain's settings, and there's no server-side templating. To control tracking per email, use the [Email API](/docs/email-api/send-email/). ## Related - [Headers and metadata](/docs/email-api/headers-and-metadata/) - [SMTP headers](/docs/smtp/headers/) - [DNS records](/docs/dictionary/dns-records/) - [Unsubscribes](/docs/audiences/unsubscribes/) --- Source: https://emailit.com/docs/dictionary/email-headers/ --- # Enhanced status codes > RFC 3463 enhanced status codes such as 5.1.1, 4.7.0 and 5.7.26 explained, with the usual cause behind each and what to change before you send again. Enhanced status codes are the `x.y.z` codes that follow the three-digit reply in most SMTP responses. They're more precise than the [reply code](/docs/dictionary/smtp-reply-codes/), so they're the fastest way to understand why an email bounced or was deferred. ## How the code is built An enhanced status code has three parts, `class.subject.detail`: ```text 550 5.1.1 The email account that you tried to reach does not exist. │ │ └─ detail: bad destination mailbox address │ └─── subject: addressing └───── class: permanent failure ``` | Class | Meaning | |---|---| | `2` | Success. | | `4` | Persistent transient failure. The message might be delivered if it's sent again later. | | `5` | Permanent failure. Sending the same message again won't work. | | Subject | Area | |---|---| | `0` | Other or undefined. | | `1` | Addressing: the sender or recipient address. | | `2` | Mailbox: the recipient's mailbox, such as full or disabled. | | `3` | Mail system: the receiving system, such as storage or size limits. | | `4` | Network and routing: DNS, connections and timeouts. | | `5` | Mail delivery protocol: SMTP commands and syntax. | | `6` | Message content or media: encoding and conversion. | | `7` | Security or policy: authentication, spam filtering and reputation. | Emailit stores the enhanced code of every failed delivery. You see it with the full reply on the email's **Deliveries** tab, and as `smtp_enhanced_code` in `email.attempted` webhook events. Whether Emailit retries depends on the reply code and message, as described in [How Emailit handles replies](/docs/dictionary/smtp-reply-codes/#how-emailit-handles-replies-from-recipient-servers). ## Success codes | Code | Meaning | Notes | |---|---|---| | `2.0.0` | Success. | Emailit's SMTP relay replies `250 2.0.0 OK: queued as em_...` when it accepts your message. | | `2.1.5` | Destination address valid. | The recipient was accepted. | | `2.6.0` | Message accepted. | Common in replies from Microsoft servers. | ## Temporary failures (4.x.x) | Code | Meaning | Common cause | What to do | |---|---|---|---| | `4.2.1` | Mailbox temporarily unavailable. | The mailbox is receiving mail too quickly, or it's inactive. | Nothing for a single message. Emailit treats Gmail's "receiving mail at a rate" and "inactive mailbox" replies as permanent. | | `4.2.2` | Mailbox full. | The recipient is over their storage quota. | Emailit treats common over-quota replies as hard bounces. Remove the address if it keeps happening. | | `4.3.0` | Mail system problem. | A temporary fault on the receiving server. | Nothing. Emailit retries. | | `4.4.1` | No answer from host. | The recipient's server didn't respond or the connection timed out. | Nothing for a single message. If a whole domain is affected, check that its MX records work. | | `4.4.2` | Bad connection. | The connection dropped during the transaction. | Nothing. Emailit retries. | | `4.4.5` | System congestion. | The receiving server is overloaded. Emailit's relay also uses this code when you exceed your per-second [sending limit](/docs/limits/). | Slow down. For Emailit's relay, keep under your per-second limit. | | `4.5.3` | Too many recipients. | Too many recipients in one transaction. Emailit's relay also uses this code when you hit your daily sending limit. | For Emailit's relay, wait for the daily reset at 00:00 UTC or request a higher limit. | | `4.7.0` | Temporary security or policy rejection. | Greylisting, or a provider throttling mail it considers suspicious, such as Gmail's "unusual rate of unsolicited mail". | Let Emailit retry. If it persists, check [sending health](/docs/deliverability/sending-health/), complaint rates and authentication. | | `4.7.1` | Delivery temporarily not authorized. | Greylisting or a temporary policy block. | Let Emailit retry. | | `4.7.28` | Rate limited (Gmail). | Gmail detected an unusual rate of unsolicited mail from your IP or domain. | Reduce volume to Gmail, clean your list and review complaints. | ## Permanent failures (5.x.x) | Code | Meaning | Common cause | What to do | |---|---|---|---| | `5.0.0` | Permanent failure, no detail. | A generic rejection. | Read the message text for the reason. | | `5.1.0` | Address error. | The address was rejected for an unspecified addressing reason. | Check the address for typos. | | `5.1.1` | Bad destination mailbox. | The mailbox doesn't exist. | Remove the address. Emailit suppresses it if it hard bounces again within 24 hours. | | `5.1.2` | Bad destination system. | The recipient's domain doesn't exist or has no mail server. | Check the domain for typos, such as `gmial.com`. | | `5.1.3` | Bad destination address syntax. | The address isn't valid. | Fix the address, or [verify](/docs/email-verification/) your list before sending. | | `5.1.8` | Bad sender's system address. | The receiving server couldn't resolve the envelope sender's domain. | Check that your return-path MX and SPF records on `emailit.yourdomain.com` are published. See [DNS records](/docs/domains/dns-records/). | | `5.1.10` | Recipient has a null MX, or recipient not found. | RFC 7505 null MX. Microsoft also uses this code when the recipient doesn't exist. | Remove the address. | | `5.2.1` | Mailbox disabled. | The account was closed or suspended. | Remove the address. | | `5.2.2` | Mailbox full. | The recipient is over quota. | Try again much later, or remove the address if it keeps happening. | | `5.2.3` | Message too long for the mailbox. | The message exceeds the recipient's size limit. | Reduce the message size. | | `5.3.4` | Message too big for the system. | The message exceeds the receiving server's size limit. | Send smaller attachments, or link to files instead. | | `5.4.1` | No answer from host, or access denied. | Microsoft uses it when a recipient is rejected at the edge, often because the address doesn't exist. | Check the address and remove it if it's invalid. | | `5.4.4` | Unable to route. | The recipient's domain has no usable MX or A record. | Check the domain. | | `5.5.0` | Protocol error. | The server rejected an SMTP command. | Contact support if it affects many recipients. | | `5.7.0` | Security or policy rejection. | A generic policy block, often for spam or reputation. | Read the message text, and review content and list quality. | | `5.7.1` | Delivery not authorized, message refused. | The recipient's server blocked the message or your IP: spam filtering, a blocklist, or the recipient only accepts mail from certain senders. | Read the message text. For blocklists, see [best practices](/docs/deliverability/best-practices/). Emailit pauses that IP-to-domain route for an hour on non-content blocks. | | `5.7.8` | Authentication credentials invalid. | Wrong username or password, typically seen when your app logs in to an SMTP server. | For Emailit's relay, use an API key as the password. | | `5.7.9` | Authentication mechanism too weak. | The server requires a stronger login method. | Use `PLAIN` or `LOGIN` over TLS with Emailit's relay. | | `5.7.23` | SPF validation failed. | The sending IP isn't allowed by the SPF record of the envelope sender's domain. | Make sure the TXT record on `emailit.yourdomain.com` contains `include:_spf.emailit.com`. | | `5.7.25` | Reverse DNS validation failed. | The sending IP has no matching PTR record. | Contact support. Emailit manages reverse DNS for its IPs. | | `5.7.26` | Multiple authentication checks failed, or DMARC failed. | The message failed SPF and DKIM alignment, and the domain's DMARC policy says to reject. Gmail also uses it for unauthenticated mail. | [Verify your domain](/docs/domains/verification/) so DKIM passes, and check other services that send as your domain with [DMARC reports](/docs/dmarc/reports/). | | `5.7.27` | Sender address has a null MX. | RFC 7505 null MX on the sender's domain. Gmail also uses it when SPF doesn't pass. | Check your return-path and SPF records. | | `5.7.509` | Access denied, DMARC failed (Microsoft). | Outlook.com rejected the message because it failed DMARC and your policy is `reject`. | Fix SPF and DKIM for the sending service, then review your [DMARC policy](/docs/dmarc/set-up/). | | `5.7.515` | Access denied, authentication level not met (Microsoft). | Outlook.com requires SPF, DKIM and DMARC to pass for high-volume senders. | Publish a DMARC record and make sure your domain is verified in Emailit. | | `5.7.606` | Access denied, banned sending IP (Microsoft). | The sending IP is on Microsoft's block list. | Contact support with the full reply. | > **Providers use codes differently:** The RFCs define the general meaning, but mailbox providers often reuse codes for their own reasons. The message text after the code is usually more specific, so read it before you decide what to change. ## Related - [SMTP reply codes](/docs/dictionary/smtp-reply-codes/) - [Bounce categories](/docs/dictionary/bounce-categories/) - [Bounces and complaints](/docs/deliverability/bounces-and-complaints/) - [Email details](/docs/logs/email-details/) --- Source: https://emailit.com/docs/dictionary/enhanced-status-codes/ --- # Email dictionary > Reference tables for SMTP reply codes, enhanced status codes, email headers, DNS records and bounce categories, and how Emailit handles each one. The email dictionary explains the codes, headers and records you run into when you send email. Use it when you're reading a bounce message, debugging an SMTP integration or publishing DNS records. Every page also says what Emailit does with each item, so you know whether to act. ## Reference pages - [SMTP reply codes](/docs/dictionary/smtp-reply-codes/): Three-digit server replies from 220 to 556, which ones Emailit retries, and the replies Emailit's own SMTP relay sends. - [Enhanced status codes](/docs/dictionary/enhanced-status-codes/): RFC 3463 codes such as 5.1.1 and 5.7.26, what they mean and how to fix them. - [Email headers](/docs/dictionary/email-headers/): From, Reply-To, Message-ID, DKIM-Signature, List-Unsubscribe and more, and which ones Emailit adds, rewrites or removes. - [DNS records](/docs/dictionary/dns-records/): MX, SPF, DKIM, DMARC, CNAME, PTR, BIMI, MTA-STS and TLS-RPT syntax with examples. - [Bounce categories](/docs/dictionary/bounce-categories/): Hard and soft bounces by cause, typical codes, how Emailit classifies them and what to do next. - [Glossary](/docs/glossary/): Short definitions of Emailit concepts and general email terms, from alignment to webhook secret. ## Where you see these values in Emailit - **Email detail.** Open an email in **Email API → Emails** and check the **Deliveries** tab. Each delivery attempt shows the receiving server's full reply, including the reply code and enhanced status code. - **Webhooks.** The `email.attempted` event includes `smtp_code`, `smtp_enhanced_code` and `smtp_response` for the failed attempt. See [Webhook event types](/docs/webhooks/event-types/). - **Request logs.** Replies from Emailit's own SMTP relay, such as `535 Authentication failed`, appear in **Email API → Logs**. See [Request logs](/docs/logs/request-logs/). - **Domain setup.** The exact DNS records Emailit asks you to publish are on the domain's **DNS Setup** tab. See [DNS records](/docs/domains/dns-records/). ## Related - [Email statuses](/docs/logs/email-statuses/) - [Bounces and complaints](/docs/deliverability/bounces-and-complaints/) - [SMTP troubleshooting](/docs/smtp/troubleshooting/) --- Source: https://emailit.com/docs/dictionary/ --- # SMTP reply codes > What each SMTP reply code means, whether it's temporary or permanent, how Emailit handles it, and the replies Emailit's SMTP relay sends to your app. Every SMTP command gets a three-digit reply. This page lists the codes you see in two places: replies from recipients' mail servers when Emailit delivers your mail, and replies from Emailit's own SMTP relay when your app sends through `smtp.emailit.com`. ## How to read a reply A reply starts with a three-digit code, usually followed by an [enhanced status code](/docs/dictionary/enhanced-status-codes/) and a human-readable message: ```text 550 5.1.1 : Recipient address rejected: User unknown ``` The first digit tells you the outcome: | First digit | Meaning | What the sender should do | |---|---|---| | `2` | Success. The command was accepted. | Continue. | | `3` | Intermediate. The server needs more input, for example the message body after `DATA`. | Send the next part. | | `4` | Temporary failure. The same command might work later. | Retry later. | | `5` | Permanent failure. Repeating the command won't help. | Don't retry without changing something. | The second digit gives the category (`0` syntax, `1` information, `2` connection, `5` mail system), and the third digit narrows it down. Servers often use codes loosely, so always read the enhanced code and the message text too. ## How Emailit handles replies from recipient servers When a recipient's server answers with a failure, Emailit records a [delivery](/docs/logs/email-details/) with the full reply and decides whether to retry: - **Bounced immediately.** Replies with code `550`, `551`, `553` or `554`, and replies whose text says the failure is permanent. The email gets the status `bounced`. - **Retried.** Everything else, including `421`, `450`, `451`, `452`, timeouts and connection errors. The email gets the status `attempted` and Emailit retries up to 7 times over about 21 hours (after 10, 20, 40, 80, 160, 320 and 640 minutes). If every attempt fails, the email is bounced and the address is suppressed. - **Treated as permanent despite a 4xx code.** Common "mailbox full", "over quota", "user unknown", "mailbox disabled" and "relay access denied" replies are rewritten to a permanent failure, because retrying them rarely works. - **Back-off.** A `451` reply pauses delivery from that sending IP to that recipient domain for 5 minutes. A `550 5.7.1` block (for reasons other than content) pauses it for 1 hour. Emails that hit a pause are marked `attempted` and retried on the normal schedule. Hard bounces lead to [automatic suppression](/docs/suppressions/manage/) according to your workspace settings. For causes and fixes grouped by problem, see [Bounce categories](/docs/dictionary/bounce-categories/). ## Reply code reference "Emailit" describes what happens when a recipient's server sends this code during delivery. ### 2xx and 3xx: success and intermediate | Code | Meaning | Type | Emailit | |---|---|---|---| | `220` | Service ready. The server's greeting, also sent before a `STARTTLS` handshake. | Success | Continues the conversation. | | `221` | Closing the connection, usually after `QUIT`. | Success | None. | | `235` | Authentication succeeded. | Success | Not used when delivering to recipients; Emailit's relay sends it to your app after `AUTH`. | | `250` | Requested action completed. After `DATA`, the server accepted the message. | Success | Marks the email `delivered`. | | `251` | User not local; the server will forward the message. | Success | Treated like `250`. | | `252` | The server can't verify the user but will try to deliver. | Success | Treated like `250`. | | `354` | Start sending the message body; end it with a line containing a single dot. | Intermediate | Sends the message. | ### 4xx: temporary failures | Code | Meaning | Type | Emailit | |---|---|---|---| | `421` | Service not available, closing the connection. Often too many connections or a temporary reputation block. | Temporary | Retried. Mailbox-full and account-unavailable variants are bounced. | | `450` | Mailbox unavailable, for example busy, locked or greylisted. | Temporary | Retried. Quota, unknown-user and disabled-mailbox variants are bounced. | | `451` | Local error in processing, often rate limiting or greylisting. | Temporary | Retried, with a 5-minute back-off for that IP and domain. Over-quota and inactive-mailbox variants are bounced. | | `452` | Insufficient system storage, or too many recipients in one transaction. | Temporary | Retried. Quota, storage and mailbox-full variants are bounced. | | `454` | Temporary authentication or TLS failure. | Temporary | Retried. "Relay access denied" variants are bounced. | ### 5xx: permanent failures | Code | Meaning | Type | Emailit | |---|---|---|---| | `500` | Syntax error, command not recognized. | Permanent | Retried, then bounced after the last attempt. | | `501` | Syntax error in parameters or arguments, such as a malformed address. | Permanent | Retried, then bounced after the last attempt. | | `502` | Command not implemented. | Permanent | Retried, then bounced after the last attempt. | | `503` | Bad sequence of commands. | Permanent | Retried, then bounced after the last attempt. | | `504` | Command parameter not implemented. | Permanent | Retried, then bounced after the last attempt. | | `521` | The domain doesn't accept mail (RFC 7504). | Permanent | Retried, then bounced after the last attempt. | | `530` | Authentication required, or the server requires TLS first. | Permanent | Retried, then bounced after the last attempt. | | `534` | Authentication mechanism is too weak. | Permanent | Retried, then bounced after the last attempt. | | `535` | Authentication credentials invalid. | Permanent | Retried, then bounced after the last attempt. | | `538` | Encryption required for the requested authentication mechanism. | Permanent | Retried, then bounced after the last attempt. | | `550` | Mailbox unavailable: the address doesn't exist, or the server refused the message for policy or spam reasons. | Permanent | Bounced. `550 5.7.1` blocks also trigger a 1-hour back-off for that IP and domain. | | `551` | User not local; the server won't forward. | Permanent | Bounced. | | `552` | Mailbox full or message exceeds the server's size limit. | Permanent | Retried, then bounced after the last attempt. Known over-quota replies are bounced immediately. | | `553` | Mailbox name not allowed, for example an invalid address. | Permanent | Bounced. | | `554` | Transaction failed, often a policy, spam or reputation rejection. Also sent as a greeting when a server refuses the connection. | Permanent | Bounced. | | `555` | `MAIL FROM` or `RCPT TO` parameters not recognized. | Permanent | Retried, then bounced after the last attempt. | | `556` | The domain doesn't accept mail (RFC 7504). | Permanent | Retried, then bounced after the last attempt. | > **Why some 5xx codes are retried:** Emailit bounces immediately only on `550`, `551`, `553` and `554`, or when the reply says the failure is permanent. Other `5xx` replies are retried like temporary failures and bounced after the seventh attempt. The email shows `attempted` in the meantime. ## Replies from Emailit's SMTP relay When your app sends through `smtp.emailit.com`, these are the replies Emailit can return. Replies to `MAIL` and `DATA` commands on an authenticated connection are also listed in **Email API → Logs** with their status code. | Reply | Command | Cause | What to do | |---|---|---|---| | `235 Authentication successful` | `AUTH` | The API key was accepted. | Continue. | | `535 Authentication failed` | `AUTH` | The password isn't a valid API key for any workspace. | Use a current API key as the password. The username can be `emailit`. | | `454 Temporary authentication failure` | `AUTH` | Emailit couldn't check the key because of an internal error. | Retry after a short delay. | | `504 Error: Unrecognized authentication type` | `AUTH` | Your client used a method other than `PLAIN` or `LOGIN`, such as `CRAM-MD5`. | Set the client to `PLAIN` or `LOGIN`. | | `452 4.4.5 Messages per second limit exceeded (n/limit)` | `MAIL FROM` | The workspace hit its per-second [sending limit](/docs/limits/). The limit is shared by the API and SMTP. | Slow down and retry. Most mail libraries retry `4xx` replies automatically. | | `452 4.5.3 Daily message limit exceeded (n/limit)` | `MAIL FROM` | The workspace hit its daily sending limit, which resets at 00:00 UTC. | Wait for the reset or request a higher limit from the dashboard. | | `451 Temporary local error in processing` | `MAIL FROM`, `RCPT TO`, `DATA` | A temporary problem on Emailit's side. | Retry later. | | `530 Authentication required` | `RCPT TO` | The client didn't log in before sending. | Enable SMTP authentication in your client. | | `501 Invalid RCPT TO format` | `RCPT TO` | The recipient address is malformed. | Fix the address. | | `550 Unverified workspaces can only send to workspace members' account emails.` | `RCPT TO` | The workspace is in [sandbox mode](/docs/workspaces/production-access/) and the recipient isn't a member. | Send to a member's account email, or request production access. | | `535 Mail server has been suspended` | `RCPT TO` | The workspace is suspended. | Check your sending health and contact support. | | `530 From/Sender domain is not verified for this workspace. From: ...` | `DATA` | The `From` header isn't on a verified sending domain of the workspace. Subdomains must be verified separately. | [Verify the domain](/docs/domains/verification/) or change the `From` address. | | `530 API key is restricted to sending domain: acme.com. ...` | `DATA` | The API key is restricted to one domain and the `From` address uses another. | Use the allowed domain or a different key. | | `550 Sending from this domain is paused` | `DATA` | The domain was paused, usually because of a high bounce rate. | See [Sending health](/docs/deliverability/sending-health/). | | `552 Message too large (maximum size 40MB)` | `DATA` | The message, including encoded attachments, is over 40 MB. | Send smaller attachments or link to files instead. | | `550 Loop detected` | `DATA` | The message already passed through Emailit's relay more than 4 times. | Check forwarding rules that send mail back to Emailit. | | `550 Message processing failed` | `DATA` | Emailit couldn't store the message. | Retry. If it keeps failing, contact support with the time of the attempt. | | `250 2.0.0 OK: queued as em_...` | `DATA` | Emailit accepted the message. Each recipient gets its own email ID, listed comma-separated when they fit in the reply. | Store the ID to look the email up in the dashboard or API. | The relay also sends standard protocol replies, such as `220` when you connect, `503 Error: need MAIL command` when commands arrive out of order, and `421 Timeout - closing connection` when a connection sits idle. > **Inbound mail:** When Emailit receives mail for your [inbound subdomain](/docs/inbound/set-up/) and the workspace has no credits left, the sending server gets `452 Insufficient credits to receive inbound email` and retries later. ## Related - [Enhanced status codes](/docs/dictionary/enhanced-status-codes/) - [Bounce categories](/docs/dictionary/bounce-categories/) - [SMTP settings](/docs/smtp/settings/) - [SMTP troubleshooting](/docs/smtp/troubleshooting/) --- Source: https://emailit.com/docs/dictionary/smtp-reply-codes/ --- # DMARC reports > Collect DMARC aggregate and forensic reports from mailbox providers in a hosted Emailit mailbox and see who sends as your domain and whether their mail passes. DMARC reports tell you which servers send email using your domain and whether that mail passes authentication. Emailit gives each sending domain a hosted reporting address, processes the reports mailbox providers send there, and shows the results in the dashboard. Use them to find services you forgot to authenticate and to move your DMARC policy safely toward `reject`. ## What DMARC reports are When your domain publishes a DMARC record with reporting addresses, mailbox providers such as Google, Microsoft and Yahoo send you two kinds of reports: | Report | DMARC tag | What it contains | How often | | --- | --- | --- | --- | | **Aggregate** (RUA) | `rua=` | A summary of all mail the provider saw from your domain: source IP addresses, message counts, SPF and DKIM results, and what the provider did with the mail. No message content. | Usually once a day per provider | | **Forensic** (RUF) | `ruf=` | Details of individual messages that failed DMARC, such as headers, subject and addresses. | Per failure. Few providers send them. | Aggregate reports are XML files that are hard to read by hand. Emailit parses them for you, adds the country and network of each source IP, and removes duplicates. ## How it works 1. **Turn on reports for a domain.** Emailit creates a reporting address for it, in the form `@dmarc.emailitmail.com`. 2. **Add the address to your DMARC record** as both `rua` and `ruf`. If you already have a DMARC record, add the address to it. 3. **Mailbox providers send reports** to the address, usually starting within 24 to 48 hours. 4. **Emailit processes each report** and adds it to the domain's DMARC dashboard. You can also upload reports you received elsewhere. [Set up DMARC reports](/docs/dmarc/set-up/) walks through every step. ## What you can see Go to **Email API → DMARC reports**. The list shows each sending domain with its **Status**, **Reporting address** and a **Reports** switch. Select a domain to open its reports, then pick a range of 7, 30 or 90 days. | Tab | What it shows | | --- | --- | | **Overview** | **Total volume**, **Pass rate**, **Fail volume** and number of **Reports**, a **Daily volume** chart of passing and failing mail, **Dispositions**, and the top countries and networks. | | **Sources** | Every IP address that sent as your domain, with its country, network (ASN) and pass and fail volume. | | **Countries** | Volume by country and continent. | | **ASNs** | Volume by network operator, such as Google or Amazon. | | **Reports** | Each report with its **Reporter**, **Type**, **Status**, **Source** and **Date range**. Open one to see its records. | | **Forensic** | Individual failure reports with **Arrival**, **Source IP**, **Auth failure**, **Reported domain** and **Subject**. | [Read DMARC reports](/docs/dmarc/reports/) explains what each number means and what to do about it. ## Privacy of forensic reports Forensic reports can contain personal data: recipient and sender addresses, subjects and message headers. Emailit stores them in your workspace and shows them only to its members. If you don't want to receive them, leave the `ruf` tag out of your DMARC record and keep only `rua`. ## Availability DMARC reports are included with Pro, Business and Custom plans. On Pay as you go, the DMARC reports page offers an upgrade. | | Pay as you go | Pro | Business | Custom | | --- | --- | --- | --- | --- | | DMARC reports | — | Included | Included | Included | Reports are also available through the [DMARC reports API](/docs/api-reference/dmarc/), including uploads, statistics, sources, countries and networks. ## Get started - [Set up DMARC reports](/docs/dmarc/set-up/): Turn on reports and publish your DMARC record. - [Read DMARC reports](/docs/dmarc/reports/): Pass rates, alignment, unknown senders and policy changes. --- Source: https://emailit.com/docs/dmarc/ --- # Read DMARC reports > Understand pass rate, alignment and dispositions in DMARC reports, identify unknown senders, and move your policy from none to quarantine to reject safely. This guide explains the numbers on the DMARC reports pages, how to tell your own services apart from unknown senders, and how to use the data to move your domain to an enforcing DMARC policy without blocking your own mail. ## Overview metrics Open **Email API → DMARC reports**, select a domain and choose 7, 30 or 90 days. | Metric | What it means | | --- | --- | | **Total volume** | Messages that mailbox providers reported seeing from your domain in the period. | | **Pass rate** | The share of that volume that passed DMARC: DKIM or SPF passed **and** aligned with your `From` domain. | | **Fail volume** | Messages where neither DKIM nor SPF passed with alignment. | | **Reports** | How many reports Emailit processed for the period. | | **Daily volume** | Passing and failing mail per day. A sudden spike in failures can mean spoofing or a newly added service. | | **Dispositions** | What providers did with the mail, based on your policy. | The overview also lists the top countries and networks (ASNs). Messages from IP addresses Emailit can't place are counted separately, such as "120 messages from unmapped IPs". ### Dispositions | Disposition | Meaning | | --- | --- | | `none` | Delivered as normal. This is what happens to failing mail while your policy is `p=none`. | | `quarantine` | Sent to spam or quarantine because it failed and your policy is `p=quarantine`. | | `reject` | Refused because it failed and your policy is `p=reject`. | Providers may still apply their own filtering to mail that passes. ## Alignment DMARC doesn't just check that SPF or DKIM pass. It checks that the domain they verified matches the `From` domain. Report records show the DMARC-evaluated results, and the API returns the raw checks behind them: | Field | What it tells you | | --- | --- | | **DKIM** and **SPF** | The DMARC-evaluated results, `pass` or `fail`, with alignment taken into account. Shown in the dashboard. | | `dkim_domain`, `dkim_selector`, `dkim_result` | Which domain signed the message and whether the signature verified. In the API's report records. | | `spf_domain`, `spf_result` | Which return-path domain SPF checked and whether it passed. In the API's report records. | | **Header from** | The `From` domain the message claimed. | Mail sent through Emailit should show DKIM `pass` with domain `acme.com` and selector `emailit`, and SPF `pass` for `emailit.acme.com`. DMARC only needs one of the two to pass with alignment. It's normal to see some mail where SPF fails but DKIM passes. Forwarding and mailing lists change the return path, which breaks SPF, while the DKIM signature survives. ## Identify your senders Open the **Sources** tab. Each row is an IP address that sent mail as your domain, with its country, network and pass and fail volume. Sort by **Fail** volume and work down the list. For each source, decide which group it falls into. **Services you use that pass.** Emailit, your company mailbox provider such as Google Workspace or Microsoft 365, and other tools that you've set up with SPF or DKIM. Nothing to do. **Services you use that fail.** A CRM, help desk, billing tool or website that sends as your domain without authentication. The network name usually gives it away, for example a cloud provider or the vendor's own ASN. Fix each one: - If the service supports DKIM for your domain, publish the record it gives you. - If it only supports SPF, add its `include:` to your root domain's SPF record, or have it send from a subdomain. - If you can, move that mail to Emailit, which is already authenticated. **Mail you don't recognize.** Sources in unexpected countries or networks, often with low volume and 100% failure, are usually spoofing: someone sending as your domain. Moving to `p=quarantine` or `p=reject` is how you stop them. The **Countries** and **ASNs** tabs group the same data by location and network operator, which helps when one service uses many IP addresses. ### Reports and forensic tabs The **Reports** tab lists every report, with the provider that sent it (the **Reporter**), its **Status** and **Date range**. Open a report to see when it was received and processed, and every record in it: **Source IP**, **Count**, **Country**, **ASN**, **Disposition**, **DKIM**, **SPF** and **Header from**. The **Forensic** tab lists individual failures, when providers send them. Each one shows the source, the authentication results, the original sender and recipient, the subject and the headers. Use them to track down a specific failing message. They may contain personal data, so share them carefully. ## Move to enforcement Use your reports to tighten your policy in stages. Wait at each stage until the reports look right. 1. **Start with `p=none`.** Collect reports for at least two to four weeks so you see every service, including ones that send only monthly, such as invoices. ```txt v=DMARC1; p=none; rua=mailto:k7f2m9qx4tz1@dmarc.emailitmail.com; ``` 2. **Fix every legitimate source.** Continue until all the services you use pass and the remaining failures are mail you don't recognize. 3. **Quarantine a share of failing mail.** Switch to `p=quarantine` with `pct` to apply it to part of the failing mail first, then raise `pct` to 100 over a week or two. ```txt v=DMARC1; p=quarantine; pct=25; rua=mailto:k7f2m9qx4tz1@dmarc.emailitmail.com; ``` 4. **Reject.** When quarantine has run at 100% without your own mail showing up under **Dispositions** as `quarantine`, switch to `p=reject`. ```txt v=DMARC1; p=reject; rua=mailto:k7f2m9qx4tz1@dmarc.emailitmail.com; ``` 5. **Keep watching.** Leave the `rua` address in place. A new tool that starts sending as your domain will show up as failing mail, and with `p=reject` it won't be delivered until you authenticate it. > **Subdomains follow the parent policy:** Unless you set `sp=`, a policy on `acme.com` also applies to subdomains such as `news.acme.com`. Check that mail from every subdomain passes before you enforce, or set `sp=none` until it does. ## Related - [Set up DMARC reports](/docs/dmarc/set-up/): Publish the record and upload reports. - [Best practices](/docs/deliverability/best-practices/): Authentication and alignment with Emailit. - [DMARC reports API](/docs/api-reference/dmarc/): Statistics, sources and reports as JSON. --- Source: https://emailit.com/docs/dmarc/reports/ --- # Set up DMARC reports > Turn on DMARC reports for a sending domain, publish or update your _dmarc record with Emailit's reporting address, and upload reports you already have. This guide shows how to start collecting DMARC reports for a sending domain. You turn reports on, add Emailit's reporting address to your DMARC record and wait for the first reports. You can also upload reports by hand. ## Before you begin - Your workspace is on Pro, Business or Custom. - The domain is added in **Email API → Domains**. - You can edit the domain's DNS records. ## Turn on reports **Dashboard** Go to **Email API → DMARC reports** and turn on the **Reports** switch next to the domain. You can also open the domain in **Email API → Domains** and turn on **Enable reports** in the **DMARC reports** card. The card then shows two values with copy buttons: - **Reporting address**, such as `k7f2m9qx4tz1@dmarc.emailitmail.com` - **Suggested _dmarc TXT**, a complete DMARC record that uses that address **API** Call [Update a domain](/docs/api-reference/domains/update/) with `dmarc_reports`: ```bash curl https://api.emailit.com/v2/domains/acme.com \ -X POST \ -H "Authorization: Bearer $EMAILIT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "dmarc_reports": true }' ``` The response includes the reporting address in `dmarc_address`, and the DMARC item in `dns_records` contains the suggested record. On Pay as you go, the request returns `403` with `"error": "plan_required"`. The reporting address belongs to this domain and doesn't change. If you turn reports off and on again, you get the same address. While reports are off, Emailit discards reports sent to it. ## Publish the DMARC record DMARC lives in one TXT record at `_dmarc.`. What you do depends on whether that record already exists. Check with: ```bash dig +short TXT _dmarc.acme.com @1.1.1.1 ``` ### If you don't have a DMARC record Create a TXT record at `_dmarc` with the **Suggested _dmarc TXT** value: ```txt _dmarc.acme.com. TXT "v=DMARC1; p=none; rua=mailto:k7f2m9qx4tz1@dmarc.emailitmail.com; ruf=mailto:k7f2m9qx4tz1@dmarc.emailitmail.com;" ``` `p=none` only asks for reports. It doesn't change how your mail is delivered. ### If you already have a DMARC record Don't create a second record. Receivers ignore DMARC when a domain has two. Instead, add Emailit's address to the existing `rua` and `ruf` tags, separated by commas, and keep your current policy. | Before | After | | --- | --- | | `v=DMARC1; p=none;` | `v=DMARC1; p=none; rua=mailto:k7f2m9qx4tz1@dmarc.emailitmail.com; ruf=mailto:k7f2m9qx4tz1@dmarc.emailitmail.com;` | | `v=DMARC1; p=quarantine; rua=mailto:dmarc@acme.com;` | `v=DMARC1; p=quarantine; rua=mailto:dmarc@acme.com,mailto:k7f2m9qx4tz1@dmarc.emailitmail.com; ruf=mailto:k7f2m9qx4tz1@dmarc.emailitmail.com;` | | `v=DMARC1; p=reject; rua=mailto:a@vendor.example; ruf=mailto:f@vendor.example; fo=1` | `v=DMARC1; p=reject; rua=mailto:a@vendor.example,mailto:k7f2m9qx4tz1@dmarc.emailitmail.com; ruf=mailto:f@vendor.example,mailto:k7f2m9qx4tz1@dmarc.emailitmail.com; fo=1` | Leave out the `ruf` part if you don't want forensic reports, which can contain personal data. ### If you send from a subdomain If your sending domain is `mail.acme.com` but your DMARC record is on `acme.com`, you can add the address to the `_dmarc.acme.com` record. Emailit accepts reports for the sending domain, its subdomains and its parent domain. Reports about other domains are rejected. > **Note:** [Cloudflare one-click setup](/docs/domains/cloudflare/) creates the suggested DMARC record only when the zone has none. It never edits an existing DMARC record, so merge the address yourself. ## Wait for the first reports Mailbox providers send aggregate reports about once a day, covering the previous day. The first ones usually arrive within 24 to 48 hours of publishing the record, if you sent mail to those providers in that time. Reports appear in **Email API → DMARC reports** on the domain's **Reports** tab, and the **Overview** fills in as they arrive. ## Upload a report manually If you already have reports, for example from another DMARC tool or a mailbox where they used to arrive, upload them. **Dashboard** 1. **Open the domain's reports.** In **Email API → DMARC reports**, select the domain. 2. **Select Upload report.** Choose an aggregate report (`.xml`, `.xml.gz` or `.zip`) or a forensic report (`.eml`). The file can be up to 10 MB. 3. **Check the result.** Emailit processes the file and shows one of: **Report processed successfully.**, **This report was already imported.**, **Report queued for processing.** or **Report processing failed.** **API** Call [Upload a report](/docs/api-reference/dmarc/upload/) with the file in base64: ```bash curl https://api.emailit.com/v2/domains/acme.com/dmarc/reports \ -X POST \ -H "Authorization: Bearer $EMAILIT_API_KEY" \ -H "Content-Type: application/json" \ -d "{ \"filename\": \"google.com!acme.com!1759190400!1759276799.xml.gz\", \"content_base64\": \"$(base64 < report.xml.gz | tr -d '\n')\" }" ``` Emailit responds with `202` and a report object with `"status": "pending"`. Processing runs in the background. Call [Retrieve a report](/docs/api-reference/dmarc/get/) to see whether it became `processed`, `duplicate` or `failed`. Files over 10 MB return `413`. Uploads work even when the **Reports** switch is off. Emailit detects duplicates, so uploading the same report twice doesn't double your numbers. A report about a different domain fails with "Reported domain … does not match …". ## Troubleshooting | Problem | Fix | | --- | --- | | No reports after 48 hours | Check that `dig +short TXT _dmarc.acme.com` returns exactly one record and that it contains your reporting address. Make sure the **Reports** switch is on. | | Reports from only one or two providers | Providers only report on mail they received. Low-volume domains often get few reports. | | No forensic reports | Normal. Many providers, including Gmail, don't send them. | | `403 plan_required` when turning on reports | DMARC reports need Pro, Business or Custom. | ## Related - [Read DMARC reports](/docs/dmarc/reports/): Understand pass rates and move to enforcement. - [DNS records](/docs/domains/dns-records/#dmarc-txt-optional): How DMARC fits with your other records. --- Source: https://emailit.com/docs/dmarc/set-up/ --- # Add a sending domain > Add your domain to Emailit, publish the DNS records it generates, send them to a developer if needed, and verify the domain so you can send. This guide walks you through adding a sending domain, publishing its DNS records and verifying it. It takes a few minutes of work, plus however long your DNS provider takes to publish the records. ## Before you begin - You need access to the DNS settings of the domain, or someone who has it. You can [email the records to them](#send-the-records-to-someone-else). - Decide which domain you'll send from. Using a subdomain such as `mail.acme.com` keeps your sending reputation separate from your root domain. See [Root domain or subdomain?](/docs/domains/#root-domain-or-subdomain) - Check that your plan has room for another domain. See [Domain limits](/docs/domains/limits/). - For the API, use an API key with **Full Access**. Sending-only keys can't manage domains. ## Domain name rules - Enter the bare domain: `acme.com` or `mail.acme.com`. Don't include `http://`, `https://` or a `www.` prefix. - Use letters, digits, hyphens and dots, with a top-level domain of at least two letters. Emailit stores the name in lowercase. - Subdomains at any depth are allowed, for example `eu.mail.acme.com`. Each one is a separate domain with its own records. - A name can only be added once per workspace. Adding it again returns `409 Domain with this name already exists`. ## Add the domain **Dashboard** 1. **Open Domains.** Go to **Email API → Domains** and select **Add domain**. If the button is disabled, your workspace has reached its domain limit. The badge next to the page title shows how many you've used. 2. **Enter the domain.** In **Name**, type the domain you send from, for example `mail.acme.com`, and select **Create**. 3. **Review the records.** Emailit opens the domain page on the **DNS Setup** tab. It lists each record with its type, name, value, priority and TTL, and a copy button for each value. **API** Call [Create a domain](/docs/api-reference/domains/create/) with the domain name. ```bash curl https://api.emailit.com/v2/domains \ -X POST \ -H "Authorization: Bearer $EMAILIT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "mail.acme.com" }' ``` The `201` response includes the domain's `id` and a `dns_records` array with every record to publish: ```json { "object": "domain", "id": "dom_2kq8Vt4xLm7Rz", "name": "mail.acme.com", "verification_status": "pending", "manual_review_required": false, "spf_status": "pending", "dkim_status": "pending", "return_path_status": "pending", "dns_records": [ { "required": true, "type": "MX", "name": "emailit.mail.acme.com", "value": "feedback-smtp.ffdc-1.emailit.com", "priority": 10, "ttl": "auto", "status": "pending", "error": null }, { "required": true, "type": "TXT", "name": "emailit.mail.acme.com", "value": "v=spf1 include:_spf.emailit.com ~all", "priority": null, "ttl": "auto", "status": "pending", "error": null } ] } ``` The full response lists all six records, including DKIM, DMARC, tracking and inbound. You can also set these optional fields when you create the domain: | Field | Default | Description | | --- | --- | --- | | `tracking_key` | `go` | Subdomain prefix for the tracking CNAME. See [Custom tracking domain](/docs/tracking/custom-tracking-domain/). | | `inbound_key` | `inbound` | Subdomain prefix for the inbound MX record. | | `dmarc_reports` | `false` | Adds Emailit's reporting address to the suggested DMARC record. Pro, Business and Custom only; otherwise `403 plan_required`. | `track_loads` and `track_clicks` can't be turned on when you create a domain, because tracking needs a verified tracking CNAME first. Sending `true` returns `422`. There's no separate switch for outgoing or incoming mail. A verified domain can send, and it receives inbound email as soon as its inbound MX record is published. The API accepts `outgoing` and `incoming` flags for compatibility, but they don't change how the domain behaves. ## Publish the DNS records Add the records at the company that hosts your domain's DNS. That's often your registrar (GoDaddy, Namecheap) or a DNS service (Cloudflare, Amazon Route 53). | Record | Type | Host | What to do | | --- | --- | --- | --- | | Return path | MX | `emailit.` | Required. Priority 10. | | SPF | TXT | `emailit.` | Required. | | DKIM | TXT | `emailit._domainkey.` | Required. Paste the whole value. | | DMARC | TXT | `_dmarc.` | Recommended. Skip it if the domain already has a DMARC record. | | Tracking | CNAME | `go.` | Only if you want open and click tracking. | | Inbound | MX | `inbound.` | Only if you want to receive email. Priority 10. | Most DNS providers want only the part before your domain in the host field (`emailit`, not `emailit.acme.com`). [DNS records](/docs/domains/dns-records/) has the exact values, provider-specific tips and common mistakes. If your domain uses Cloudflare DNS, the domain page offers **Set up with Cloudflare**, which creates the records for you. See [Set up DNS with Cloudflare](/docs/domains/cloudflare/). ## Send the records to someone else If someone else manages your DNS, email them the records from the dashboard. 1. **Open the domain.** In **Email API → Domains**, select the domain. 2. **Select Send to email.** It's in the top-right corner of the **DNS Setup** card. 3. **Enter their address.** In **Recipient Email**, type the address of your developer or IT administrator and select **Send Instructions**. They receive an email from Emailit with every record and its value. Ask them to tell you when the records are published so you can run the check. ## Check DNS and verify **Dashboard** On the domain page, select **Check DNS**. Emailit looks up every record and updates the status next to each one: **OK**, **Missing**, **Invalid** or **Not checked**. Hover over **Missing** or **Invalid** to see what Emailit found. **API** Call [Verify a domain](/docs/api-reference/domains/verify/). You can use the domain ID or its name. ```bash curl https://api.emailit.com/v2/domains/mail.acme.com/verify \ -X POST \ -H "Authorization: Bearer $EMAILIT_API_KEY" ``` The response contains the updated domain. Check `verification_status` and the `status` and `error` of each item in `dns_records`. > **Verification only runs when you ask for it:** Emailit doesn't verify a new domain on its own. After you publish or fix records, run **Check DNS** again. DNS changes usually appear within minutes, but some providers take up to 48 hours. The domain is verified when SPF, DKIM and the return path all show **OK**. DMARC, tracking and inbound are optional and don't affect verification. ## Verify it worked - The domain shows **Verified** in **Email API → Domains**, and the **SPF**, **DKIM** and **Return Path** columns show **OK**. - Send a test message from an address on the domain. On the **Emails** page, select **Compose**, or call [Send an email](/docs/api-reference/emails/send/). If the domain shows **Pending verification**, it's waiting for a manual review. This applies to Pay as you go domains registered less than 30 days ago. See [Domain verification](/docs/domains/verification/#pending-verification). > **Note:** A verified domain doesn't lift sandbox mode. Until your workspace has production access, you can only send to the account emails of workspace members. See [Production access](/docs/workspaces/production-access/). ## Troubleshooting | Problem | Fix | | --- | --- | | **Add domain** is disabled | You've reached your plan's domain limit. Delete an unused domain or raise the limit. See [Domain limits](/docs/domains/limits/). | | `The domain must be in apex format` | Remove `http://`, `https://` or `www.` and make sure the name ends in a real top-level domain. | | Records show **Missing** after an hour | The host is probably doubled, for example `emailit.acme.com.acme.com`. Enter only `emailit` in the host field. | | DKIM shows **Invalid** | Part of the value was cut off. Copy it again with the copy button. | [Domain verification](/docs/domains/verification/#troubleshooting) covers more cases. ## Related - [DNS records](/docs/domains/dns-records/): Reference for every record Emailit generates. - [Domain verification](/docs/domains/verification/): Statuses, manual review and daily re-checks. - [Custom tracking domain](/docs/tracking/custom-tracking-domain/): Set up open and click tracking. - [Set up inbound email](/docs/inbound/set-up/): Receive email on your domain. --- Source: https://emailit.com/docs/domains/add-a-domain/ --- # Set up DNS with Cloudflare > Create every DNS record for a sending domain in your Cloudflare zone in one step with a scoped API token, then re-sync or disconnect it later. If your domain's DNS is hosted on Cloudflare, Emailit can create all of its DNS records for you. You paste a Cloudflare API token that's limited to the zone, and Emailit adds the records, then checks DNS right away. This guide shows how to create the token, run the setup, re-sync records and disconnect. ## Before you begin - [Add the domain](/docs/domains/add-a-domain/) to Emailit. - The domain's nameservers must be Cloudflare's. Emailit detects this automatically, including Cloudflare's Foundation DNS nameservers. For a subdomain such as `mail.acme.com`, the parent zone `acme.com` must be on Cloudflare. - You need a Cloudflare account that can create API tokens for the zone. When Emailit detects Cloudflare, the **DNS Setup** card on the domain page shows a banner titled **This domain uses Cloudflare DNS** with a **Set up with Cloudflare** button. If you don't see it, the domain's nameservers aren't Cloudflare's. Add the records manually as described in [DNS records](/docs/domains/dns-records/). ## Create a Cloudflare API token The token only needs to read the zone and edit its DNS records. 1. **Open API tokens.** In Cloudflare, go to **My Profile > API Tokens** (`https://dash.cloudflare.com/profile/api-tokens`) and select **Create Token**. 2. **Start from the Edit zone DNS template.** It grants **Zone > DNS > Edit**. 3. **Add Zone Read.** Add a second permission, **Zone > Zone > Read**. Emailit needs it to find the zone. 4. **Limit it to your zone.** Under **Zone Resources**, select **Include > Specific zone** and pick the zone, for example `acme.com`. 5. **Create and copy the token.** Cloudflare shows the token once. Keep the page open until you've pasted it into Emailit. ## Create the records 1. **Open the domain.** In **Email API → Domains**, select the domain and stay on the **DNS Setup** tab. 2. **Select Set up with Cloudflare.** The **Set up DNS with Cloudflare** dialog opens and shows the zone and its nameservers. 3. **Paste the token.** Paste it into **Cloudflare API token**. Emailit stores it encrypted and never shows it again. 4. **Choose the optional records.** Under **Records to create**, the return path, SPF and DKIM records are always included. DMARC, tracking and inbound are selected by default. Clear any you don't want. 5. **Select Create records.** Emailit creates the records, then runs **Check DNS** for you. The dialog then lists each record with what happened to it: | Result | Meaning | | --- | --- | | **Created** | The record didn't exist and was added. | | **Updated** | A record with the same name and type existed with a different value. Emailit replaced its value. | | **Already set** | An identical record already existed. Nothing changed. | | **Kept existing** | A DMARC record already existed. Emailit never changes an existing DMARC record. | | **Failed** | Cloudflare rejected this record. The message from Cloudflare is shown under it. The other records are still applied. | If every required record passes, the dialog says **The domain is verified.** Otherwise, wait a few minutes and select **Check DNS** on the domain page. > **Existing records with the same name are replaced:** If the zone already has a record with the same name and type, for example an MX record at `inbound.acme.com`, Emailit overwrites its value. Clear the **Inbound** or **Tracking** checkbox if those hosts are already in use for something else. Your root records, such as the MX and SPF on `acme.com`, are never touched. ### How the records are created - **TTL** is set to Auto. - **The tracking CNAME is DNS only** (not proxied), which tracking requires. - **Each record gets a comment** in Cloudflare so you can tell it was created by Emailit. - **DMARC** is only created when the zone has no `_dmarc` record. To add Emailit's reporting address to an existing record, edit it yourself. See [Set up DMARC reports](/docs/dmarc/set-up/). ## Sync records later After the first setup, the banner changes to **Connected to Cloudflare** and shows when you last ran the setup. Emailit doesn't watch the zone or change it on its own. Select **Sync records** to apply the records again with the stored token, for example after you: - change the tracking subdomain under **Custom Subdomains**, or - turn on DMARC reports for the domain, or - deleted one of the records by mistake. If the last run didn't finish cleanly, the banner shows the error. Fix the cause, then select **Sync records** again. ## Disconnect Cloudflare Select **Disconnect** on the banner and confirm. Emailit deletes the stored API token. The DNS records it created stay in your Cloudflare zone, so the domain keeps working. You can also revoke the token in Cloudflare at any time. Deleting the domain from Emailit also deletes the stored token. ## Troubleshooting | Error | Cause and fix | | --- | --- | | `Cloudflare rejected the API token. Check that it was copied fully and is still active.` | The token is incomplete, expired or revoked. Create a new one and paste it again. | | `No Cloudflare zone for acme.com is visible to this token.` | The token doesn't include the zone or lacks **Zone > Zone > Read**. Edit the token's permissions and zone resources. | | `Cloudflare API error: …` with an authentication message | The token lacks **Zone > DNS > Edit**. Nothing was created. Fix the permissions and try again. | | A record shows **Failed** | Cloudflare refused that record, often because a CNAME exists at the same host. Remove the conflicting record in Cloudflare, then select **Sync records**. | | Tracking still shows **Invalid** | Someone switched the CNAME to **Proxied**. Set it back to **DNS only** and select **Check DNS**. | ## Related - [DNS records](/docs/domains/dns-records/): What each record does. - [Domain verification](/docs/domains/verification/): Statuses and daily re-checks. --- Source: https://emailit.com/docs/domains/cloudflare/ --- # DNS records for sending domains > Every DNS record Emailit generates for a sending domain, what each one does, how to enter it at your DNS provider and how to check it with dig. This page lists every DNS record Emailit generates for a sending domain and explains why each one exists. Use it while you publish records, or when a record shows **Missing** or **Invalid** on the domain page. ## All records The examples use `acme.com`. Replace it with your sending domain. If you send from a subdomain such as `mail.acme.com`, every host below moves under it, for example `emailit.mail.acme.com`. | Purpose | Type | Host | Value | Priority | Required | | --- | --- | --- | --- | --- | --- | | Return path | MX | `emailit.acme.com` | `feedback-smtp.ffdc-1.emailit.com` | 10 | Yes | | SPF | TXT | `emailit.acme.com` | `v=spf1 include:_spf.emailit.com ~all` | – | Yes | | DKIM | TXT | `emailit._domainkey.acme.com` | `v=DKIM1; t=s; h=sha256; p=MIIBIjANBgkqh…` (your public key) | – | Yes | | DMARC | TXT | `_dmarc.acme.com` | `v=DMARC1; p=none;` | – | No | | Tracking | CNAME | `go.acme.com` | `go.emailitmail.com` | – | No | | Inbound | MX | `inbound.acme.com` | `inbound.emailitmail.com` | 10 | No | The DKIM key is unique to each domain, so always copy it from the domain page or from the `dns_records` array of [Retrieve a domain](/docs/api-reference/domains/get/). The tracking and inbound hosts change if you set a custom `tracking_key` or `inbound_key`. When [DMARC reports](/docs/dmarc/) are on, the suggested DMARC value also includes your reporting address. A domain is verified when the return path, SPF and DKIM records all pass. The other three records turn on optional features and never block verification. ## Return path (MX) ```txt emailit.acme.com. MX 10 feedback-smtp.ffdc-1.emailit.com. ``` Every message Emailit sends uses an envelope sender (return path) on this subdomain, in the form `@emailit.acme.com`. Receiving servers send bounces and delivery reports to that address. The MX record routes them back to Emailit, which matches them to the original message and updates its status. The check passes when `emailit.acme.com` has exactly one MX record and it points to `feedback-smtp.ffdc-1.emailit.com`. A second MX record on the same host makes it **Invalid**. ## SPF (TXT on the return-path subdomain) ```txt emailit.acme.com. TXT "v=spf1 include:_spf.emailit.com ~all" ``` SPF tells receivers which servers may send mail for a domain. Receivers check SPF against the return-path domain, not the `From` address. Because Emailit's return path is `emailit.acme.com`, that's the host that needs the SPF record. This setup has two benefits: - **You don't touch your root SPF record.** Your existing `acme.com` SPF record for Google Workspace, Microsoft 365 or other services stays as it is. Adding `include:_spf.emailit.com` to the root record isn't needed and only uses up one of SPF's 10 DNS lookups. - **SPF still aligns for DMARC.** DMARC's default relaxed alignment accepts a return path on a subdomain of the `From` domain. Mail from `hello@acme.com` with return path `emailit.acme.com` passes SPF alignment. The check passes when a TXT record starting with `v=spf1` at `emailit.acme.com` includes `_spf.emailit.com`. Publish only one SPF record per host; two SPF records on the same host make SPF fail at receivers. ## DKIM (TXT) ```txt emailit._domainkey.acme.com. TXT "v=DKIM1; t=s; h=sha256; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA…" ``` Emailit signs every message with a 2048-bit RSA key using the selector `emailit` and `d=acme.com`. Receivers fetch the public key from this record to check the signature. A valid signature proves the message came from you and wasn't changed in transit, and because the signing domain matches your `From` domain, DKIM aligns for DMARC. The value is about 420 characters long, which is longer than the 255-character limit of a single TXT string. Most DNS providers split it for you. If yours asks you to do it, split the value into quoted chunks in one record: `"v=DKIM1; t=s; h=sha256; p=MIIB…" "…IDAQAB;"`. Emailit joins the chunks before it checks the key. The check compares the DKIM tags, not the exact text, so extra spaces or a different tag order are fine. It fails if the `p=` key doesn't match. ## DMARC (TXT, optional) ```txt _dmarc.acme.com. TXT "v=DMARC1; p=none;" ``` DMARC tells receivers what to do with mail that claims to be from your domain but fails SPF and DKIM alignment, and where to send reports about it. Gmail and Yahoo require a DMARC record from bulk senders, so publish one even though Emailit doesn't need it for verification. - **Start with `p=none`.** It changes nothing about delivery and lets you collect reports first. - **Move to `p=quarantine`, then `p=reject`,** once reports show that all legitimate mail passes. See [Read DMARC reports](/docs/dmarc/reports/) for a safe rollout. - **Keep one DMARC record.** If `_dmarc.acme.com` already exists, don't add a second one. Edit the existing record instead. With [DMARC reports](/docs/dmarc/) on, the suggested value adds `rua` and `ruf` addresses at `dmarc.emailitmail.com`. [Set up DMARC reports](/docs/dmarc/set-up/) shows how to merge them into an existing record. Emailit only looks at `_dmarc` on the exact sending domain. If you send from `mail.acme.com` and rely on the policy published at `_dmarc.acme.com`, receivers apply the parent policy, but the DMARC row on the domain page won't show **OK**. ## Tracking (CNAME, optional) ```txt go.acme.com. CNAME go.emailitmail.com. ``` Open and click tracking uses a hostname on your own domain. Emailit rewrites links to `https://go.acme.com/` and loads the open pixel from the same host. Without a verified CNAME, mail is sent untracked. See [Custom tracking domain](/docs/tracking/custom-tracking-domain/). The CNAME must point straight at `go.emailitmail.com`. If your DNS provider proxies records (Cloudflare's orange cloud), set this record to **DNS only**. ## Inbound (MX, optional) ```txt inbound.acme.com. MX 10 inbound.emailitmail.com. ``` With this record, Emailit accepts mail for any address at `inbound.acme.com`, such as `support@inbound.acme.com`, and delivers it to your workspace as an inbound email. The check requires priority 10. Inbound mail only works for verified domains. See [Set up inbound email](/docs/inbound/set-up/). ## Enter hosts at your DNS provider DNS providers name the host field differently, and most of them add your domain to whatever you type. Enter the **relative** name (the part before your domain) unless your provider asks for the full name. | Record | Relative host (most providers) | Full name (FQDN) | | --- | --- | --- | | Return path and SPF | `emailit` | `emailit.acme.com` | | DKIM | `emailit._domainkey` | `emailit._domainkey.acme.com` | | DMARC | `_dmarc` | `_dmarc.acme.com` | | Tracking | `go` | `go.acme.com` | | Inbound | `inbound` | `inbound.acme.com` | For a subdomain such as `mail.acme.com` hosted in the `acme.com` zone, the relative host includes the subdomain: `emailit.mail`, `emailit._domainkey.mail`, `_dmarc.mail`, `go.mail` and `inbound.mail`. | Provider | Host field | Notes | | --- | --- | --- | | Cloudflare | **Name** | Accepts the relative or full name. Set the tracking CNAME to **DNS only**. Or use [one-click setup](/docs/domains/cloudflare/). | | GoDaddy | **Name** | Relative name only. Typing the full name creates `emailit.acme.com.acme.com`. | | Namecheap | **Host** | Relative name only. MX records go in the **Mail Settings** section, set to **Custom MX**. | | Amazon Route 53 | **Record name** | Relative name; the console shows the zone after the field. Enter MX values as `10 feedback-smtp.ffdc-1.emailit.com`. Wrap TXT values in double quotes and split the DKIM value into quoted chunks. | | DigitalOcean | **Hostname** | Relative name. | ## TTL Emailit shows the TTL as `auto`. Use your provider's default or automatic TTL. A short TTL such as 300 seconds helps while you set things up, because corrections reach resolvers faster. TTL doesn't affect verification. ## Check records with dig Query a record directly to see what the rest of the internet sees. These commands use the public resolver `1.1.1.1` so a cached answer from your network doesn't mislead you. ```bash dig +short MX emailit.acme.com @1.1.1.1 dig +short TXT emailit.acme.com @1.1.1.1 dig +short TXT emailit._domainkey.acme.com @1.1.1.1 dig +short TXT _dmarc.acme.com @1.1.1.1 dig +short CNAME go.acme.com @1.1.1.1 dig +short MX inbound.acme.com @1.1.1.1 ``` Expected answers: ```txt 10 feedback-smtp.ffdc-1.emailit.com. "v=spf1 include:_spf.emailit.com ~all" "v=DKIM1; t=s; h=sha256; p=MIIBIjANBgkqh…" "…IDAQAB;" "v=DMARC1; p=none;" go.emailitmail.com. 10 inbound.emailitmail.com. ``` An empty answer means the record isn't published at that name yet. On Windows, use `nslookup -type=TXT emailit.acme.com 1.1.1.1`. ## Common mistakes | Mistake | Symptom | Fix | | --- | --- | --- | | Domain added twice to the host | **Missing**. The record exists at `emailit.acme.com.acme.com`. | Enter only the relative host, such as `emailit`. | | Tracking CNAME proxied | Tracking shows **Invalid**. The host returns Cloudflare IP addresses instead of a CNAME. | Switch the record to **DNS only** (grey cloud). | | Emailit's SPF added to the root record only | SPF shows **Missing**. | Add the SPF record at `emailit.acme.com`. You don't need it on the root domain. | | Two SPF records on `emailit.acme.com` | Emailit may show **OK**, but receivers see an SPF error. | Keep a single `v=spf1` record on that host. | | Trailing dot handled differently | Value becomes `go.emailitmail.com.acme.com`. | Some providers treat a value without a trailing dot as relative. Enter `go.emailitmail.com.` with a trailing dot, or follow the provider's example. | | DKIM value cut off | DKIM shows **Invalid**. | Copy the whole value with the copy button. Split it into quoted chunks if the provider limits length. | | Extra MX on the return-path host | Return path shows **Invalid**. | Keep exactly one MX record on `emailit.acme.com`. | | Inbound MX with another priority | Inbound shows **Invalid**. | Set the priority to 10. | | Second DMARC record added | Receivers ignore DMARC for the domain. | Merge everything into one `_dmarc` record. | ## Related - [Add a domain](/docs/domains/add-a-domain/): Step-by-step setup. - [Domain verification](/docs/domains/verification/): Statuses and troubleshooting. - [DNS record types](/docs/dictionary/dns-records/): Background on MX, TXT, CNAME and more. - [Email best practices](/docs/deliverability/best-practices/): Authentication, DMARC alignment and list hygiene. --- Source: https://emailit.com/docs/domains/dns-records/ --- # Sending domains > Why Emailit needs your own domain, which DNS records it sets up for authentication, tracking and inbound mail, and how domains stay verified. Every email you send through Emailit comes from a sending domain that you own and have verified with DNS records. This page explains what Emailit sets up on that domain, how verification works and how many domains your plan includes. ## Why you need a sending domain Mailbox providers such as Gmail, Outlook and Yahoo only trust mail that proves it comes from the domain in the `From` address. Emailit proves that with SPF and DKIM records on your domain, so your messages are authenticated as yours and build your domain's reputation, not a shared one. Until a domain is verified, Emailit rejects messages from it. The API returns `422 Domain not verified` and SMTP replies `530 From/Sender domain is not verified for this workspace`. Every `From` address must use a verified domain in the same workspace, and each subdomain counts as its own domain. ## What Emailit configures When you add a domain, Emailit generates a 2048-bit DKIM key and gives you up to six DNS records. Three are required; the rest switch on optional features. | Record | Host | Purpose | Required | | --- | --- | --- | --- | | MX | `emailit.` | Return path. Receives bounces and delivery reports for your mail. | Yes | | TXT (SPF) | `emailit.` | Authorizes Emailit's servers to send for the return-path subdomain. | Yes | | TXT (DKIM) | `emailit._domainkey.` | Public key that verifies the DKIM signature on every message. | Yes | | TXT (DMARC) | `_dmarc.` | Tells receivers what to do with mail that fails authentication. | No | | CNAME | `go.` | Custom tracking domain for open and click tracking. | No | | MX | `inbound.` | Receives inbound email at any address on `inbound.`. | No | The SPF record lives on the return-path subdomain, not on your root domain, so you don't need to change an existing SPF record for Google Workspace, Microsoft 365 or another provider. See [DNS records](/docs/domains/dns-records/) for every value and why it's set up this way. ## How it works 1. **You add the domain** in the dashboard or with the API. Emailit creates the DNS records for it. 2. **You publish the records** at your DNS provider, or let Emailit create them in [Cloudflare](/docs/domains/cloudflare/). 3. **You run Check DNS.** Emailit looks up the records. When SPF, DKIM and the return path all pass, the domain is verified. 4. **You send.** Emailit signs every message with your DKIM key and uses `emailit.` as the return path, so SPF and DKIM both align with your `From` domain for DMARC. 5. **Emailit re-checks DNS every day.** If a required record breaks, the domain stops sending and the workspace owner receives an email titled "Sending domain `` is no longer verified". ## Domain statuses | Status | Meaning | | --- | --- | | **Verified** | SPF, DKIM and the return path pass. You can send from the domain. | | **Not verified** | One or more required records are missing or invalid, or you haven't run Check DNS yet. | | **Pending verification** | DNS may be correct, but the domain is waiting for a manual review. This applies to domains registered less than 30 days ago on Pay as you go. | | **Sending paused** | The domain's bounce rate is too high. A banner shows on the domain page. See [Sending health](/docs/deliverability/sending-health/). | [Domain verification](/docs/domains/verification/) explains each status, the per-record checks and how to fix common failures. ## Limits | | Pay as you go | Pro | Business | Custom | | --- | --- | --- | --- | --- | | Sending domains | 3 (25 after your first credit purchase) | 100 | 1,000 | By agreement | | Review of domains under 30 days old | Yes | No | No | Yes | AppSumo licenses set their own domain allowance. See [Domain limits](/docs/domains/limits/). ## Root domain or subdomain? You can verify a root domain such as `acme.com` or a subdomain such as `mail.acme.com`. Both work. Pick based on how you send: - **Use a subdomain for each type of mail** if you send both transactional and marketing email, for example `notify.acme.com` for receipts and password resets, and `news.acme.com` for campaigns. Each subdomain builds its own reputation, so a campaign that draws complaints doesn't hurt your receipts. - **Use the root domain** if you only send a modest amount of transactional mail and want your `From` address to be `hello@acme.com`. - **Send from exactly the domain you verified.** Verifying `acme.com` doesn't let you send from `mail.acme.com`, and the other way around. Your existing email keeps working either way: Emailit's records sit on their own hosts (`emailit.`, `emailit._domainkey.`, `go.` and `inbound.`), so they don't replace your root MX or SPF records. ## Get started - [Add a domain](/docs/domains/add-a-domain/): Add your domain and publish its DNS records. - [DNS records](/docs/domains/dns-records/): Every record, its value and how to enter it. - [Set up with Cloudflare](/docs/domains/cloudflare/): Create all records in your Cloudflare zone in one step. - [Domain verification](/docs/domains/verification/): Statuses, daily re-checks and troubleshooting. --- Source: https://emailit.com/docs/domains/ --- # Domain limits > How many sending domains each Emailit plan and AppSumo license includes, what happens when you reach the limit and how to raise it. Each workspace can add a set number of sending domains, based on its plan or license. This page lists the limits, explains what you see when you reach one and how to get more room. ## Domains per plan | | Pay as you go | Pro | Business | Custom | | --- | --- | --- | --- | --- | | Sending domains | 3 (25 after your first credit purchase) | 100 | 1,000 | By agreement | - **Pay as you go** starts at 3 domains. After your first credit purchase in the workspace, the limit rises to 25 and stays there. - **Pro** and **Business** have fixed limits of 100 and 1,000 domains. - **Custom** plans include the number of domains in your agreement. Every domain counts toward the limit, whether it's verified or not, including each subdomain you add separately. ## AppSumo licenses When an AppSumo license is active in a workspace, its domain allowance replaces the plan's. | AppSumo tier | Sending domains | | --- | --- | | Tier 1 | 1 | | Tier 2 | 2 | | Tiers 3 to 6 | Unlimited | See [AppSumo](/docs/billing/appsumo/) for everything else a license includes. ## See your usage In **Email API → Domains**, a badge next to the page title shows domains used and the limit, for example `2/3`. Hover over it to see which plan sets the limit, such as "Pay as you go plan allows up to 3 domains." Workspaces with an unlimited license don't show a badge. With the API, [List domains](/docs/api-reference/domains/list/) returns the same numbers next to the list: ```json { "data": [], "next_page_url": null, "previous_page_url": null, "domain_limit": 3, "domain_count": 2, "plan_name": "Pay as you go" } ``` `domain_limit` is `null` when the workspace has no limit. `plan_name` is `License` when an AppSumo license sets the limit. ## When you reach the limit - **Dashboard:** the **Add domain** button is disabled. - **API:** [Create a domain](/docs/api-reference/domains/create/) returns `422` with a message that names the limit: ```json { "error": "Pay as you go includes 3 domain(s)." } ``` With an AppSumo license, the message is `This license includes 1 domain(s).` Domains you already have keep working. The limit only stops new domains from being added. ## Raise your limit | Current plan | How to get more domains | | --- | --- | | Pay as you go | Buy credits once in **Workspace → Billing** to go from 3 to 25 domains, or upgrade to Pro. | | Pro | Upgrade to Business for 1,000 domains. | | Business | [Contact sales](/contact/) about a Custom plan. | | AppSumo tier 1 or 2 | Upgrade your license tier on AppSumo. Tiers 3 and higher are unlimited. | You can also free a slot by deleting a domain you no longer use. Only workspace admins can delete domains, and deleting one drops any messages from it that are still being sent. ## Related - [Plans](/docs/billing/plans/): Compare Pay as you go, Pro and Business. - [Buy credits](/docs/billing/credits/): How credit purchases work. - [All limits](/docs/limits/): Sending, webhook and audience limits in one place. - [Add a domain](/docs/domains/add-a-domain/): Add and verify a sending domain. --- Source: https://emailit.com/docs/domains/limits/ --- # Domain verification > How Emailit verifies sending domains, what each domain and record status means, how manual review and daily re-checks work, and how to fix failures. A sending domain must be verified before Emailit sends mail from it. This page explains what Emailit checks, what each status means, how the review for new Pay as you go domains works and how to fix a domain that won't verify. ## What Emailit checks Verification looks at three DNS records. All three must pass: | Check | Record | Passes when | | --- | --- | --- | | **SPF** | TXT at `emailit.` | A `v=spf1` record includes `_spf.emailit.com`. | | **DKIM** | TXT at `emailit._domainkey.` | The record's `p=` key matches the domain's DKIM key. | | **Return Path** | MX at `emailit.` | There's exactly one MX record and it points to `feedback-smtp.ffdc-1.emailit.com`. | Emailit also checks DMARC, the tracking CNAME and the inbound MX record at the same time and shows their status, but they don't affect verification. See [DNS records](/docs/domains/dns-records/) for every value. On Pay as you go, Emailit also checks the domain's age. See [Pending verification](#pending-verification). ## Domain statuses The **Verification** column in **Email API → Domains** and the API field `verification_status` show one of three states. | Dashboard | API value | Meaning | Can send? | | --- | --- | --- | --- | | **Verified** | `verified` | SPF, DKIM and the return path pass. | Yes | | **Not verified** | `pending` | A required record is missing or invalid, or DNS hasn't been checked yet. | No | | **Pending verification** | `pending_review` | The domain is waiting for a manual review by Emailit. | No | The API also returns `verified_at` (when the domain last passed), `dns_checked_at` (when DNS was last checked) and `manual_review_required`. ## Record statuses Each record on the domain page, and each item in the API's `dns_records` array, has its own status. | Dashboard | API value | Meaning | | --- | --- | --- | | **OK** | `ok` | The record is published with the expected value. | | **Missing** | `missing` | Emailit found no matching record at that host. | | **Invalid** | `invalid` | A record exists, but its value is wrong, for example an MX pointing elsewhere or a DKIM key that doesn't match. | | **Not checked** | `pending` | DNS hasn't been checked since the record was created or its host changed. | DMARC can also return `error` when the lookup itself fails, which the dashboard shows as **Not checked**. Hover over **Missing** or **Invalid** in the dashboard, or read the record's `error` field in the API, to see exactly what Emailit found. ## What verification unlocks - **Sending.** You can send from any address on the domain through the API, SMTP, campaigns and automations. - **Inbound email.** Emailit only accepts mail for `inbound.` on verified domains. - **Production access.** You need at least one verified domain before you can [request production access](/docs/workspaces/production-access/). - **Tracking,** once the tracking CNAME also shows **OK**. See [Open and click tracking](/docs/tracking/). ## Run a check Verification runs when you ask for it: - In the dashboard, select **Check DNS** on the domain page. - With the API, call [Verify a domain](/docs/api-reference/domains/verify/). - [Cloudflare setup](/docs/domains/cloudflare/) runs the check for you after it creates the records. Emailit doesn't verify a new domain on its own, so run a check after you publish or fix records. ## Pending verification On Pay as you go, a domain that was registered less than 30 days ago needs a manual review before it can send. Emailit reads the registration date of the domain from WHOIS. For a subdomain such as `mail.acme.com`, it uses the registration date of `acme.com`. If WHOIS doesn't return a date, the domain isn't held for review. While a domain waits for review: - It shows **Pending verification**, and the domain page shows a banner explaining why. - It can't send, even when all records show **OK**. - You don't need to do anything else. Keep the DNS records in place so the domain verifies as soon as it's approved. Pro and Business workspaces skip the age check. If you upgrade while a domain is pending, select **Check DNS** and it verifies as soon as DNS passes. | | Pay as you go | Pro | Business | Custom | | --- | --- | --- | --- | --- | | Review of domains under 30 days old | Yes | No | No | Yes | ### Approval is permanent Once Emailit approves a domain, the approval stays. If DNS breaks later, the domain shows **Not verified** and stops sending until you fix the records, but it doesn't go back into review. Running **Check DNS** never removes an approval. ## Daily re-check Emailit re-checks the DNS of every domain once a day and updates each record's status and **Last checked** time. - **If a verified domain fails,** Emailit marks it **Not verified** and emails the workspace owner: "Sending domain `` is no longer verified". The email lists the SPF, DKIM and return path results. Mail from the domain doesn't send until it passes again. - **If only the DNS lookups time out,** Emailit treats it as a temporary resolver problem and keeps the domain verified. - **If a domain that Emailit approved after review passes again,** the daily check restores it to **Verified**. For any other domain that lost verification, fix the records and select **Check DNS** to bring it back. The daily check doesn't verify those domains on its own. ## Paused domains A verified domain can still be paused when its bounce rate is too high. The domain page shows a **Sending paused** banner, the API returns `restricted: true`, and: - the API rejects sends from the domain with `403 Domain paused`, - SMTP replies `550 Sending from this domain is paused`, - queued mail from the domain gets the status `held`. Pausing is part of [sending health](/docs/deliverability/sending-health/), not DNS. Contact support to restore a paused domain after you've fixed the cause. ## Troubleshooting | Symptom | Likely cause | Fix | | --- | --- | --- | | SPF, DKIM or Return Path is **Missing** | The record is at the wrong host, often with the domain added twice, such as `emailit.acme.com.acme.com`. | Enter only the relative host (`emailit`, `emailit._domainkey`). Check with `dig`. See [DNS records](/docs/domains/dns-records/#check-records-with-dig). | | Everything is **OK** but the domain isn't verified | The records were published after the last check. | Select **Check DNS**. | | **Pending verification** stays for days | The domain was registered less than 30 days ago and is waiting for review. | Contact support if it's urgent, or upgrade to Pro or Business and run **Check DNS**. | | SPF is **Invalid** | The record at `emailit.` doesn't include `_spf.emailit.com`. | Use the exact value `v=spf1 include:_spf.emailit.com ~all`. | | DKIM is **Invalid** | The key was cut off, or it's from a domain you deleted and added again. Each new domain gets a new key. | Copy the current value from the domain page. | | Return Path is **Invalid** | There's a second MX record on `emailit.`, or it points elsewhere. | Keep one MX record pointing to `feedback-smtp.ffdc-1.emailit.com`. | | The domain was verified and suddenly isn't | Someone removed or changed a record, or the domain moved to a new DNS provider without the records. | Check the "no longer verified" email for which record failed, restore it and select **Check DNS**. | | Sending fails with `422 Domain not verified` | The `From` address uses a different domain or subdomain than the verified one. | Send from the exact verified domain, or add and verify the subdomain. | ## Related - [Add a domain](/docs/domains/add-a-domain/): Add a domain and publish its records. - [Domain limits](/docs/domains/limits/): How many domains your plan includes. - [Sending health](/docs/deliverability/sending-health/): Bounce rates, scores and paused domains. - [Domains API](/docs/api-reference/domains/): Create, verify and update domains. --- Source: https://emailit.com/docs/domains/verification/ --- # Attachments > Attach files to API emails as base64 content or from a URL, embed inline images with Content-ID, and stay within allowed file types and size limits. This page shows how to attach files to emails you send with `POST /emails`, either as base64-encoded content or by letting Emailit download them from a URL. It also covers inline images, allowed file types and size limits. ## Attachment fields Pass an `attachments` array. Each item is an object with these fields: - `filename` (string, required): File name shown to the recipient. It must end in an [allowed extension](#allowed-file-types). - `content` (string): The file encoded as base64. Use either `content` or `url`, not both. - `url` (string): An `http://` or `https://` URL that Emailit downloads the file from. Use either `content` or `url`, not both. - `content_type` (string): The MIME type, such as `application/pdf`. Required with `content`. With `url`, it defaults to the `Content-Type` the server returns. - `content_id` (string): A Content-ID. Setting it makes the attachment inline, so your HTML can show it with `cid:`. - `encoding` (string): How `content` is encoded. Leave it as `base64` unless you have a reason to change it. ## Attach a file as base64 Read the file, encode it as base64, and send it with its MIME type. **cURL** ```bash curl https://api.emailit.com/v2/emails \ -H "Authorization: Bearer $EMAILIT_API_KEY" \ -H "Content-Type: application/json" \ -d "{ \"from\": \"Acme Billing \", \"to\": \"ada@example.com\", \"subject\": \"Invoice INV-1042\", \"text\": \"Your invoice is attached.\", \"attachments\": [{ \"filename\": \"INV-1042.pdf\", \"content\": \"$(base64 < INV-1042.pdf | tr -d '\n')\", \"content_type\": \"application/pdf\" }] }" ``` **Node.js** ```javascript import { readFile } from 'node:fs/promises'; import { Emailit } from '@emailit/node'; const emailit = new Emailit(process.env.EMAILIT_API_KEY); const pdf = await readFile('INV-1042.pdf'); await emailit.emails.send({ from: 'Acme Billing ', to: 'ada@example.com', subject: 'Invoice INV-1042', text: 'Your invoice is attached.', attachments: [ { filename: 'INV-1042.pdf', content: pdf.toString('base64'), content_type: 'application/pdf', }, ], }); ``` **Python** ```python import base64 import os from emailit import EmailitClient client = EmailitClient(os.environ["EMAILIT_API_KEY"]) with open("INV-1042.pdf", "rb") as f: pdf = base64.b64encode(f.read()).decode("ascii") client.emails.send({ "from": "Acme Billing ", "to": "ada@example.com", "subject": "Invoice INV-1042", "text": "Your invoice is attached.", "attachments": [{ "filename": "INV-1042.pdf", "content": pdf, "content_type": "application/pdf", }], }) ``` **PHP** ```php $emailit = Emailit::client(getenv('EMAILIT_API_KEY')); $emailit->emails()->send([ 'from' => 'Acme Billing ', 'to' => 'ada@example.com', 'subject' => 'Invoice INV-1042', 'text' => 'Your invoice is attached.', 'attachments' => [[ 'filename' => 'INV-1042.pdf', 'content' => base64_encode(file_get_contents('INV-1042.pdf')), 'content_type' => 'application/pdf', ]], ]); ``` ## Attach a file from a URL Instead of encoding the file yourself, give Emailit a `url`. Emailit downloads the file while it builds the message, so the request takes as long as the download. ```json { "from": "Acme Billing ", "to": "ada@example.com", "subject": "Invoice INV-1042", "text": "Your invoice is attached.", "attachments": [ { "filename": "INV-1042.pdf", "url": "https://files.acme.com/invoices/INV-1042.pdf" } ] } ``` The URL has to meet these rules, or the request fails with `422` and `Attachment error`: - It uses `http` or `https` and points to a public host. Private and internal addresses are refused. - It returns the file directly with a `2xx` status. Redirects aren't followed. - The download finishes within 30 seconds. - The file is no larger than 25 MB, as reported by its `Content-Length` header. Emailit builds a separate copy of the message for each recipient and downloads URL attachments for each copy. Make sure signed or expiring URLs stay valid for the whole request, and that the file host can handle one download per recipient. ## Embed inline images To show an image inside the HTML body instead of as a separate attachment, give it a `content_id` and reference that ID with `cid:` in an `img` tag. ```json { "from": "Acme ", "to": "ada@example.com", "subject": "Your weekly report", "html": "

\"Weekly

", "attachments": [ { "filename": "chart.png", "content": "iVBORw0KGgoAAAANSUhEUgAA...", "content_type": "image/png", "content_id": "chart-week-40" } ] } ``` The `content_id` in the attachment and the value after `cid:` must match exactly. Inline images make the message larger for every recipient, so for logos and other shared images, a hosted image URL in the HTML is usually the better choice. ## Size limits | Limit | Value | | --- | --- | | Whole message, after encoding | 40 MB. Larger messages fail with `413 Message too large`. | | One attachment downloaded from `url` | 25 MB | | Download timeout for `url` | 30 seconds | | JSON request body | 50 MB | Base64 encoding makes files about a third larger, and the 40 MB limit applies to the encoded message. In practice, keep the combined size of your files under about 29 MB. For anything bigger, upload the file to your own storage and send a link. ## Allowed file types `filename` must end in one of these extensions. Any other extension, or a name without an extension, fails validation with `400`. | Category | Extensions | | --- | --- | | Text | `.txt`, `.csv`, `.log`, `.css`, `.ics`, `.xml` | | Images | `.jpg`, `.jpe`, `.jpeg`, `.gif`, `.png`, `.bmp`, `.psd`, `.tif`, `.tiff`, `.svg`, `.indd`, `.ai`, `.eps` | | Documents | `.doc`, `.docx`, `.rtf`, `.odt`, `.ott`, `.pdf`, `.pub`, `.pages`, `.mobi`, `.epub` | | Audio | `.mp3`, `.m4a`, `.m4v`, `.wma`, `.ogg`, `.flac`, `.wav`, `.aif`, `.aifc`, `.aiff` | | Video | `.mp4`, `.mov`, `.avi`, `.mkv`, `.mpeg`, `.mpg`, `.wmv` | | Spreadsheets | `.xls`, `.xlsx`, `.ods`, `.numbers` | | Presentations | `.odp`, `.ppt`, `.pptx`, `.pps`, `.key` | | Archives and contacts | `.zip`, `.vcf` | | Email | `.eml` | | Signatures and encryption | `.p7c`, `.p7m`, `.p7s`, `.pgp`, `.asc`, `.sig` | Executable files and scripts aren't on the list and can't be attached. ## Read the attachments of a sent email [List attachments](/docs/api-reference/emails/attachments/) (`GET /emails/{id}/attachments`) returns each attachment with its `filename`, `content_type`, `size`, `content_id`, `content_disposition` (`attachment` or `inline`) and base64 `content`. It needs a **Full Access** key. Attachments are deleted together with the message contents when your [data retention](/docs/data-retention/) period ends. ## Troubleshooting | Error | What to check | | --- | --- | | `Attachment at index 0 missing content_type (required when using 'content')` | Add `content_type` to every base64 attachment. | | `Attachment 'report.exe' has unsupported file type '.exe'` | Use an allowed extension, or put the file in a `.zip`. | | `Attachment at index 0 cannot have both 'content' and 'url'` | Send one of the two. | | `Attachment error` with `Failed to fetch attachment` | The URL returned an error status, redirected, timed out or isn't public. Open it from a server outside your network to check. | | `Attachment error` with `Attachment too large (max 25MB)` | Host the file and send a link instead. | | `413 Message too large` | Reduce the total size of attachments. | ## Related - [Send an email](/docs/email-api/send-email/) - [Send an email](/docs/api-reference/emails/send/) API reference - [Data retention](/docs/data-retention/) --- Source: https://emailit.com/docs/email-api/attachments/ --- # Headers and metadata > Add custom email headers and List-Unsubscribe to API sends, see which headers Emailit adds or rewrites, and attach metadata that comes back in webhooks. This page covers two ways to add your own information to an email sent with the Email API: `headers`, which become part of the message the recipient gets, and `meta`, which Emailit stores with the email and returns in the API and in webhooks. It also lists the headers Emailit adds, rewrites or removes. ## Add custom headers Pass `headers` as an object of header names and string values: ```json { "from": "Acme ", "to": "ada@example.com", "subject": "Receipt for order 1042", "text": "Thanks for your order.", "headers": { "X-Entity-Ref-ID": "order-1042", "X-Acme-Account": "881" } } ``` - Use the request fields, not `headers`, for From, To, Cc, Bcc, Reply-To and Subject. - Don't set headers whose names start with `X-Emailit-`. Emailit uses them internally. For example, a message that already contains `X-Emailit-ID` is treated as processed and skips Emailit's header rewriting and DKIM signing. - Headers that Emailit sets itself, such as `Message-ID` and `Date`, are replaced even if you send them. See the next section. ## Headers Emailit adds or changes | Header | What Emailit does | | --- | --- | | `Message-ID` | Sets it to ``, the same value as `message_id` in the send response. A `Message-ID` you provide is replaced. | | `Date` | Sets it when Emailit first processes the message for delivery. | | `Subject` | Writes the final subject and encodes non-ASCII characters. | | `Return-Path` | Sets a bounce address on your return-path subdomain, `emailit.`, so bounces come back to Emailit and SPF aligns. | | `DKIM-Signature` | Signs the message with your domain's DKIM key. A second signature for `emailitmail.com` can be added for complaint feedback loops. | | `Received` | Adds trace headers for the API and the Emailit mail server. | | `X-Emailit-ID` | Adds the email's token. | | `Feedback-ID` | Adds an identifier that mailbox providers use in complaint reports. | | `X-Emailit-Meta` | Adds your `meta` values, base64-encoded, when you send `meta`. | | `X-Emailit-Tracking` | Adds the requested settings when you turn tracking on with `tracking`. | | `Bcc` | Removes it, so Bcc recipients stay hidden. | | `Reply-To` | Removes it when it names the same address as From. | | `Content-Disposition` | Removes it from the top level of the message. Attachment parts keep theirs. | The [SMTP relay](/docs/smtp/headers/) applies the same rewriting to messages you submit over SMTP. ## Add List-Unsubscribe to bulk mail Mailbox providers such as Gmail and Yahoo expect a one-click unsubscribe option on promotional and other bulk mail. [Campaigns](/docs/campaigns/) add one automatically. For newsletters or digests you send through the API, add both headers yourself: ```json { "from": "Acme ", "to": "ada@example.com", "subject": "Acme weekly digest", "html": "

This week at Acme…

", "headers": { "List-Unsubscribe": ", ", "List-Unsubscribe-Post": "List-Unsubscribe=One-Click" } } ``` - The `https` URL must accept a `POST` request with the body `List-Unsubscribe=One-Click` and unsubscribe the person without asking them to confirm (RFC 8058). - Make each URL specific to the recipient, so your endpoint knows who to unsubscribe. - Emailit includes `List-Unsubscribe` and `List-Unsubscribe-Post` in the DKIM signature, which providers require for one-click unsubscribe. See [How do I meet Gmail and Yahoo bulk sender requirements?](/docs/kb/gmail-yahoo-bulk-sender-requirements/) for the other requirements. When someone unsubscribes, stop sending to them. You can add them to your [suppression list](/docs/suppressions/) so Emailit blocks future sends. ## Attach metadata `meta` is an object of string keys and string values that Emailit stores with each email. Use it to connect an email to records in your own system. ```json { "from": "Acme ", "to": "ada@example.com", "subject": "Receipt for order 1042", "text": "Thanks for your order.", "meta": { "order_id": "1042", "customer_id": "cus_881", "kind": "receipt" } } ``` Convert numbers and booleans to strings before you send them. Emailit returns `meta`: - In [Retrieve an email](/docs/api-reference/emails/get/), [Retrieve metadata](/docs/api-reference/emails/meta/) and [List emails](/docs/api-reference/emails/list/). - In webhook events for the email: under `data.object.meta` for `email.accepted`, `email.scheduled`, `email.canceled` and the delivery events, and under `data.object.email.meta` for `email.loaded` and `email.clicked`. A delivery event with metadata looks like this (trimmed): ```json [ { "type": "email.delivered", "data": { "object": { "id": "em_33VtK8mRq1xZp7LwN4cY2bHsDfa", "object": "email", "to": "ada@example.com", "subject": "Receipt for order 1042", "status": "delivered", "meta": { "order_id": "1042", "customer_id": "cus_881", "kind": "receipt" } } } } ] ``` [Retrying](/docs/email-api/retry-and-forward/) an email keeps its metadata. Forwarding creates a new email without it. > **Metadata travels with the message:** Emailit also writes `meta` into the message as the base64-encoded `X-Emailit-Meta` header, so anyone who views the raw message can decode it. Don't put secrets, tokens or sensitive personal data in `meta`. ## Find emails later You can't search or filter emails by `meta`. To find an email again: - **Store the IDs.** Save the `id`, or the `ids` map for several recipients, next to your own record, and look the email up with [Retrieve an email](/docs/api-reference/emails/get/). - **Filter the list.** [List emails](/docs/api-reference/emails/list/) filters on `to`, `from`, `subject`, `status`, `created_at`, `updated_at`, `spam_score`, `api_key_id` and `sending_domain_id`. See [Filtering](/docs/api-reference/filtering/). - **Use separate API keys.** Give each application or feature its own [API key](/docs/developers/api-keys/), then filter by `api_key_id`, or by **API key** in **Email API → Emails**. - **Match webhook events.** Read `meta` from each event to route it to the right record as it arrives. ## Related - [Send an email](/docs/email-api/send-email/) - [SMTP headers](/docs/smtp/headers/) - [Webhook event types](/docs/webhooks/event-types/) - [Email headers dictionary](/docs/dictionary/email-headers/) --- Source: https://emailit.com/docs/email-api/headers-and-metadata/ --- # Idempotent requests > Use the Idempotency-Key header to retry send and forward requests safely. Key format, the 24-hour window, replays, 409 and 503 responses, and key strategies. Networks fail. When a send request times out, you can't tell whether Emailit received it, and sending it again might email your customer twice. An `Idempotency-Key` header makes the retry safe: Emailit processes the first request and returns the same response for any repeat with the same key. ## How it works Add an `Idempotency-Key` header to `POST /emails` or `POST /emails/{id}/forward`. 1. **First request.** Emailit reserves the key for your workspace and processes the request. 2. **Success.** Emailit stores the response for 24 hours. Any request with the same key in that window gets the stored response back with `200`, and no new email is created. 3. **Failure.** If the request fails, for example with a `400` or `402`, Emailit releases the key. Fix the problem and retry with the same key. 4. **Overlap.** If a second request arrives while the first is still running, it gets `409` and nothing is sent. Retry shortly with the same key. Keys are scoped to your workspace, so two workspaces can use the same key without conflict. > **The key, not the body, identifies the request:** Emailit doesn't compare request bodies. A repeat with the same key returns the original response even if the body is different. Use a new key for every distinct email. ## Key format | Rule | Value | | --- | --- | | Length | 1 to 256 characters | | Characters | Letters `A–Z` and `a–z`, digits `0–9`, hyphen `-` and underscore `_` | | Scope | Per workspace | | Window | 24 hours after the first successful response | A key with other characters, such as `:` or `/`, is rejected with `400 Invalid Idempotency-Key`. ## Choose a key Derive the key from the event that causes the email, so every retry path produces the same key: | Email | Example key | | --- | --- | | Order receipt | `order-1042-receipt` | | Password reset | `password-reset-7f3c9a1e` (the reset token's ID) | | Weekly digest | `digest-user-881-2026-w40` | | Background job | The job's ID, or a UUID you generate when you enqueue the job and store with it | Avoid keys that change between attempts, such as timestamps or a UUID generated inside the retry loop. They make every retry look like a new request. ## Send with an idempotency key The Node.js, Python and PHP examples retry on network errors and on `409`, `429` and `5xx` responses, reusing the same key each time. The cURL example uses curl's built-in retry, which covers timeouts, `429` and most `5xx` responses. **cURL** ```bash curl https://api.emailit.com/v2/emails \ -H "Authorization: Bearer $EMAILIT_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: order-1042-receipt" \ --retry 3 \ -d '{ "from": "Acme ", "to": "ada@example.com", "subject": "Receipt for order 1042", "text": "Thanks for your order." }' ``` **Node.js** ```javascript async function sendOnce(payload, key, attempts = 4) { for (let attempt = 1; attempt <= attempts; attempt++) { try { const res = await fetch('https://api.emailit.com/v2/emails', { method: 'POST', headers: { Authorization: `Bearer ${process.env.EMAILIT_API_KEY}`, 'Content-Type': 'application/json', 'Idempotency-Key': key, }, body: JSON.stringify(payload), }); if (res.ok) return res.json(); if (![409, 429].includes(res.status) && res.status < 500) { throw new Error(`Send failed: ${res.status} ${await res.text()}`); } const wait = Number(res.headers.get('retry-after')) || attempt * 2; await new Promise((r) => setTimeout(r, wait * 1000)); } catch (err) { if (err.message.startsWith('Send failed') || attempt === attempts) throw err; await new Promise((r) => setTimeout(r, attempt * 2000)); } } throw new Error('Send failed after retries'); } const email = await sendOnce( { from: 'Acme ', to: 'ada@example.com', subject: 'Receipt for order 1042', text: 'Thanks for your order.', }, 'order-1042-receipt', ); ``` **Python** ```python import os import time import requests def send_once(payload, key, attempts=4): for attempt in range(1, attempts + 1): try: res = requests.post( "https://api.emailit.com/v2/emails", headers={ "Authorization": f"Bearer {os.environ['EMAILIT_API_KEY']}", "Idempotency-Key": key, }, json=payload, timeout=30, ) except requests.RequestException: if attempt == attempts: raise time.sleep(attempt * 2) continue if res.ok: return res.json() if res.status_code not in (409, 429) and res.status_code < 500: res.raise_for_status() time.sleep(int(res.headers.get("retry-after", attempt * 2))) raise RuntimeError("Send failed after retries") email = send_once( { "from": "Acme ", "to": "ada@example.com", "subject": "Receipt for order 1042", "text": "Thanks for your order.", }, "order-1042-receipt", ) ``` **PHP** ```php function sendOnce(array $payload, string $key, int $attempts = 4): array { for ($attempt = 1; $attempt <= $attempts; $attempt++) { $ch = curl_init('https://api.emailit.com/v2/emails'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 30, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('EMAILIT_API_KEY'), 'Content-Type: application/json', 'Idempotency-Key: ' . $key, ], CURLOPT_POSTFIELDS => json_encode($payload), ]); $body = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); curl_close($ch); if ($body !== false && $status >= 200 && $status < 300) { return json_decode($body, true); } if ($body !== false && !in_array($status, [409, 429]) && $status < 500) { throw new RuntimeException("Send failed: $status $body"); } sleep($attempt * 2); } throw new RuntimeException('Send failed after retries'); } $email = sendOnce([ 'from' => 'Acme ', 'to' => 'ada@example.com', 'subject' => 'Receipt for order 1042', 'text' => 'Thanks for your order.', ], 'order-1042-receipt'); ``` ## Responses | Status | When | What to do | | --- | --- | --- | | `200` | First successful request, or a replay of it within 24 hours | Use the response. A replay has the same body, including the same `id`. | | `400 Invalid Idempotency-Key` | The key is empty, too long or has invalid characters | Fix the key. | | `409 Idempotency key in progress` | A request with the same key is still being processed | Wait a moment, then retry with the same key. | | `503 Idempotency unavailable` | Emailit couldn't reach its idempotency store, so it refused the request rather than risk a duplicate | Retry with the same key. | | Any other error | The request failed and the key was released | Fix the cause and retry with the same key. | Rate limits are checked before the key, so a retry can still get `429`. Wait for the `retry-after` header and send the same key again. ## Related - [Idempotency](/docs/api-reference/idempotency/) in the API reference - [Send an email](/docs/email-api/send-email/) - [Forward an email](/docs/email-api/retry-and-forward/) - [Rate limits](/docs/api-reference/rate-limits/) - [Why are my emails sent twice?](/docs/kb/duplicate-emails-sent/) --- Source: https://emailit.com/docs/email-api/idempotency/ --- # Email API > Send transactional email with one HTTPS request, then schedule, cancel, retry or forward it. Base URL, authentication, features and limits. The Email API sends email from your application over HTTPS instead of an SMTP connection. Use it for transactional mail such as sign-up confirmations, password resets, receipts and alerts, especially when you want templates, scheduling, idempotent retries and a separate ID for every recipient. ## How it works 1. Your application calls `POST /emails` with a From address on a verified sending domain, the recipients, and the content or a template. 2. Emailit validates the request, charges 1 credit per recipient and creates one email per recipient, each with its own `em_` ID. 3. The response comes back right away with the status `accepted`, or `scheduled` if you set a send time. Delivery happens in the background. 4. Emailit signs the message with DKIM for your domain, runs spam checks and delivers it. Temporary failures are retried for about 21 hours. 5. Every status change appears in **Email API → Emails** and is sent to your [webhooks](/docs/webhooks/). ## Base URL and authentication | Item | Value | | --- | --- | | Base URL | `https://api.emailit.com/v2` | | Authentication | `Authorization: Bearer secret_••••` with an [API key](/docs/developers/api-keys/) | | Request body | JSON, sent with `Content-Type: application/json` | | Send endpoint | `POST /emails` | A **Full Access** key can call every endpoint. A **Sending Only** key can send, reschedule, cancel, retry and forward email, and you can restrict it to a single sending domain. See [Authentication](/docs/api-reference/authentication/) for details. ## Send an email **cURL** ```bash curl https://api.emailit.com/v2/emails \ -H "Authorization: Bearer $EMAILIT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "from": "Acme ", "to": "ada@example.com", "subject": "Welcome to Acme", "html": "

Thanks for signing up, Ada.

", "text": "Thanks for signing up, Ada." }' ``` **Node.js** ```javascript import { Emailit } from '@emailit/node'; const emailit = new Emailit(process.env.EMAILIT_API_KEY); const email = await emailit.emails.send({ from: 'Acme ', to: 'ada@example.com', subject: 'Welcome to Acme', html: '

Thanks for signing up, Ada.

', text: 'Thanks for signing up, Ada.', }); console.log(email.id); ``` **Python** ```python import os from emailit import EmailitClient client = EmailitClient(os.environ["EMAILIT_API_KEY"]) email = client.emails.send({ "from": "Acme ", "to": "ada@example.com", "subject": "Welcome to Acme", "html": "

Thanks for signing up, Ada.

", "text": "Thanks for signing up, Ada.", }) ``` **PHP** ```php $emailit = Emailit::client(getenv('EMAILIT_API_KEY')); $email = $emailit->emails()->send([ 'from' => 'Acme ', 'to' => 'ada@example.com', 'subject' => 'Welcome to Acme', 'html' => '

Thanks for signing up, Ada.

', 'text' => 'Thanks for signing up, Ada.', ]); ``` A successful request returns `200` with the new email: ```json { "object": "email", "id": "em_33VtK8mRq1xZp7LwN4cY2bHsDfa", "token": "33VtK8m4XcPq2RwZ7nLb1YsTgHd", "message_id": "<33VtK8m4XcPq2RwZ7nLb1YsTgHd@acme.com>", "from": "Acme ", "to": ["ada@example.com"], "subject": "Welcome to Acme", "status": "accepted", "scheduled_at": null, "created_at": "2026-10-01T09:30:12.418203Z", "tracking": { "loads": false, "clicks": false } } ``` New workspaces start in sandbox mode and can only send to the account email addresses of workspace members. [Request production access](/docs/workspaces/production-access/) before you send to anyone else. ## What you can do - [Send an email](/docs/email-api/send-email/): From addresses, recipients, content, templates, tracking and every error. - [Attachments](/docs/email-api/attachments/): Attach files as base64 or from a URL, and embed inline images. - [Scheduling](/docs/email-api/scheduling/): Send later, reschedule, or cancel an email before it goes out. - [Idempotency](/docs/email-api/idempotency/): Retry requests safely without sending the same email twice. - [Headers and metadata](/docs/email-api/headers-and-metadata/): Custom headers, List-Unsubscribe, and metadata echoed in webhooks. - [Retry and forward](/docs/email-api/retry-and-forward/): Resend failed or held email, or forward a sent email to someone else. - [Templates](/docs/templates/): Store designs once and send them by alias with Temple variables. - [Emails API reference](/docs/api-reference/emails/): Every email endpoint with parameters and responses. ## Limits | Limit | Value | | --- | --- | | Recipients per request | 50 in `to`, 50 in `cc` and 50 in `bcc` | | Message size | 40 MB, including encoded attachments | | Attachment downloaded from a URL | 25 MB, with a 30-second download timeout | | Idempotency window | 24 hours | | Sending rate (default) | 2 emails per second and 5,000 emails per day per workspace, shared with SMTP | | Forwarding | 3 forwards per hour per workspace | | Reschedule or cancel a scheduled email | Until 3 minutes before its send time | | Retry window | 30 days after the original email was created | Rate limits count recipients, so one request to 10 recipients uses 10 of your per-second and daily allowance. Pro and Business workspaces get automatic increases based on sending health, and any workspace can ask for more from the **Sending Limits** card on the dashboard home page. See [Limits](/docs/limits/) and [Rate limits](/docs/api-reference/rate-limits/). ## Credits Every recipient costs 1 credit, and `to`, `cc` and `bcc` addresses all count. If the workspace doesn't have enough credits for every recipient, the request fails with `402` and nothing is sent. Retries and forwards are charged as new sends. | Action | Credits | | --- | --- | | Email sent with the API or SMTP (per recipient) | 1 | | Inbound email received | 1 | | Campaign email (per recipient) | 2 | | Automation run | 3 | | Email verification (per address) | 5 | See [Credits](/docs/billing/credits/) for how included and purchased credits are used. ## Next steps - [API quickstart](/docs/quickstart/api/): Send your first email in a few minutes. - [Add a sending domain](/docs/domains/add-a-domain/): Verify the domain you send from. - [Set up webhooks](/docs/webhooks/set-up/): Get delivery, bounce and engagement events. - [API or SMTP?](/docs/get-started/api-or-smtp/): Compare the Email API with the SMTP relay. --- Source: https://emailit.com/docs/email-api/ --- # Retry and forward emails > Resend a bounced, failed, suppressed or held email as a new email, or forward a sent email to another recipient, from the dashboard or the API. Retry sends an email again with the same content after it bounced, failed, was suppressed or was held. Forward sends a copy of an email you already sent to someone else, for example a support colleague or a customer who lost the original. Both create a new email with its own ID and leave the original unchanged. ## Before you begin - In the API, both endpoints work with **Full Access** and **Sending Only** keys. - Both are charged as new sends, so you need enough [credits](/docs/billing/credits/). - Both need the original message contents. Emailit deletes them when your [data retention](/docs/data-retention/) period for message contents ends: | | Pay as you go | Pro | Business | Custom | | --- | --- | --- | --- | --- | | Message contents kept | 7 days | 30 days | 30 days | Flexible | ## Retry an email An email can be retried when all of these are true: | Requirement | Details | | --- | --- | | Status | `bounced`, `failed`, `suppressed` or `held` | | Age | Created less than 30 days ago | | Contents | The message contents haven't been deleted by data retention | | Sending domain | The original sending domain still exists in the workspace | A retry creates a **new email** with a new `em_` ID and a new Message-ID. It reuses the original's raw message, recipient, metadata and tracking settings, and goes through the normal delivery pipeline. It costs 1 credit, or 2 credits if the original was a campaign email. A workspace in sandbox mode can only retry emails addressed to workspace members. Fix the cause before you retry, or the new email ends up with the same status: - **Suppressed:** remove the address from [suppressions](/docs/suppressions/manage/) first. - **Held for credits:** top up your [credits](/docs/billing/credits/). - **Held because the domain was paused:** resolve the [sending health](/docs/deliverability/sending-health/) issue. - **Held for spam score:** a retry sends the same content and is likely to be held again. Change the content and send a new email instead. See [Spam checks](/docs/deliverability/spam-checks/). - **Bounced:** check the bounce reason on the email's detail page. A mailbox that doesn't exist will bounce again. If Emailit added the address to your suppressions after the bounce, remove it first. **Dashboard** 1. Go to **Email API → Emails** and open the email. 2. Select **Retry** at the top of the page. The dashboard shows it on held and suppressed emails; retry bounced and failed emails with the API. 3. Select **Retry** again in the **Retry Email** dialog to confirm. The new email appears in the list with its own ID. **API** Call [Retry an email](/docs/api-reference/emails/retry/) (`POST /emails/{id}/retry`) with the original email's ID. There's no request body. **cURL** ```bash curl -X POST https://api.emailit.com/v2/emails/em_33VtK8mRq1xZp7LwN4cY2bHsDfa/retry \ -H "Authorization: Bearer $EMAILIT_API_KEY" ``` **Node.js** ```javascript const retried = await emailit.emails.retry('em_33VtK8mRq1xZp7LwN4cY2bHsDfa'); ``` **Python** ```python retried = client.emails.retry("em_33VtK8mRq1xZp7LwN4cY2bHsDfa") ``` **PHP** ```php $retried = $emailit->emails()->retry('em_33VtK8mRq1xZp7LwN4cY2bHsDfa'); ``` ```json { "object": "email", "id": "em_33Vu2LqPz8aKd4WnX6cR1tYbHgs", "original_id": "em_33VtK8mRq1xZp7LwN4cY2bHsDfa", "token": "33Vu2LqQ7mFt3XcV9bNp5KsRwEz", "message_id": "<33Vu2LqQ7mFt3XcV9bNp5KsRwEz@acme.com>", "from": "Acme ", "to": "ada@example.com", "subject": "Receipt for order 1042", "status": "accepted", "created_at": "2026-10-01T11:15:03.552918Z", "message": "Email has been queued for retry" } ``` | Error | Cause | | --- | --- | | `404 Email not found` | The ID doesn't exist in this workspace. | | `422 Cannot retry email` | The status isn't retryable, the email is older than 30 days, its contents were deleted, or its sending domain was deleted. The `message` says which. | | `402 Insufficient credits` | The workspace can't pay for the retry. | | `403 Workspace not verified` | The workspace is in sandbox mode and the recipient isn't a workspace member. | ## Forward an email Forwarding sends an outgoing email you already sent to a new recipient. Incoming (inbound) emails can't be forwarded. By default, the forward is a plain resend: the recipient gets the original subject, body and attachments as if the email had been sent to them. Set `include_headers` to send a classic forward instead, with a "Forwarded message" block (original From, Date, Subject and To) and an optional comment above it. The subject then starts with `Fwd:`. - `to` (string | string[], required): The new recipients, in the same formats as `to` on a send. - `include_headers` (boolean): Add the forwarded-message block, the optional comment and the `Fwd:` subject prefix. - `comment` (string): A plain-text note shown above the forwarded message when `include_headers` is `true`. `body` is accepted as an alias. - `html` (string): An HTML note to use instead of the escaped `comment` in the HTML part, when `include_headers` is `true`. - `text` (string): A plain-text note that replaces `comment` in the text part, when `include_headers` is `true`. - `from` (string): Send from a different address. Defaults to the original From address. It must be on a verified sending domain. - `subject` (string): Replace the subject. Defaults to the original subject, or `Fwd:` plus the original subject with `include_headers`. A forward is a new send, so it follows the same rules as `POST /emails`: credits per recipient, sending rate limits, the From domain checks and the [`Idempotency-Key`](/docs/email-api/idempotency/) header all apply. Tracking follows the sending domain's settings. The original's custom headers and metadata aren't copied, and attachments are carried over only if their file type is [allowed](/docs/email-api/attachments/#allowed-file-types). Each workspace can make **3 forward requests per hour**, counting forwards from the dashboard and the API together. Over the limit, the API returns `429` with `too_many_requests`. **Dashboard** 1. Go to **Email API → Emails** and open the email. 2. Select **Forward**. 3. Enter the recipient in **To**. 4. Optional: check **Add forwarded headers and a comment** and write a **Comment**. 5. Select **Forward**. The new email appears in the list with its own ID. **API** Call [Forward an email](/docs/api-reference/emails/forward/) (`POST /emails/{id}/forward`). **cURL** ```bash curl https://api.emailit.com/v2/emails/em_33VtK8mRq1xZp7LwN4cY2bHsDfa/forward \ -H "Authorization: Bearer $EMAILIT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "to": "support@acme.com", "include_headers": true, "comment": "Customer says this receipt never arrived. Can you check?" }' ``` **Node.js** ```javascript const forwarded = await emailit.emails.forward('em_33VtK8mRq1xZp7LwN4cY2bHsDfa', { to: 'support@acme.com', include_headers: true, comment: 'Customer says this receipt never arrived. Can you check?', }); ``` **Python** ```python forwarded = client.emails.forward("em_33VtK8mRq1xZp7LwN4cY2bHsDfa", { "to": "support@acme.com", "include_headers": True, "comment": "Customer says this receipt never arrived. Can you check?", }) ``` **PHP** ```php $forwarded = $emailit->emails()->forward('em_33VtK8mRq1xZp7LwN4cY2bHsDfa', [ 'to' => 'support@acme.com', 'include_headers' => true, 'comment' => 'Customer says this receipt never arrived. Can you check?', ]); ``` The response is the same as a [send response](/docs/email-api/send-email/#read-the-response), plus `original_id` and the message "Email has been queued for forwarding". Forwarding fails with `422 Cannot forward email` if the original is an inbound email or its contents were deleted. ## Related - [Retry an email](/docs/api-reference/emails/retry/) - [Forward an email](/docs/api-reference/emails/forward/) - [Email statuses](/docs/logs/email-statuses/) - [Email details](/docs/logs/email-details/) --- Source: https://emailit.com/docs/email-api/retry-and-forward/ --- # Schedule and cancel emails > Send an email later with scheduled_at, change the send time, or cancel a scheduled, accepted or retrying email from the API or the dashboard. This page explains how to schedule an email for later with the Email API, how to move it to a different time, and how to cancel an email before it leaves. Canceling also works for emails that weren't scheduled, as long as they haven't been delivered yet. ## Schedule an email Add `scheduled_at` to a [send request](/docs/email-api/send-email/). The response has `"status": "scheduled"` and the normalized time in `scheduled_at`, and each recipient's email emits [`email.scheduled`](/docs/webhooks/events/email/scheduled/) instead of `email.accepted`. `scheduled_at` accepts these formats: | Format | Example | Notes | | --- | --- | --- | | ISO 8601 with a time zone | `2026-10-05T09:00:00Z`, `2026-10-05T09:00:00+02:00` | Recommended. Always include `Z` or an offset. | | Natural language | `tomorrow at 9am`, `in 2 hours`, `next monday 10:00`, `friday 5pm` | Interpreted in UTC, so `tomorrow at 9am` means 09:00 UTC. | A time that is now or in the past sends the email immediately with the status `accepted`. > **Check the response status:** If Emailit can't read the `scheduled_at` value, it doesn't reject the request: the email is sent right away. Check that the response has `"status": "scheduled"` and the `scheduled_at` you expected. Unix timestamps aren't recognized; convert them to ISO 8601 first. **cURL** ```bash curl https://api.emailit.com/v2/emails \ -H "Authorization: Bearer $EMAILIT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "from": "Acme ", "to": "ada@example.com", "subject": "Your appointment is tomorrow", "text": "See you at 14:00.", "scheduled_at": "2026-10-05T09:00:00Z" }' ``` **Node.js** ```javascript const email = await emailit.emails.send({ from: 'Acme ', to: 'ada@example.com', subject: 'Your appointment is tomorrow', text: 'See you at 14:00.', scheduled_at: '2026-10-05T09:00:00Z', }); ``` **Python** ```python email = client.emails.send({ "from": "Acme ", "to": "ada@example.com", "subject": "Your appointment is tomorrow", "text": "See you at 14:00.", "scheduled_at": "2026-10-05T09:00:00Z", }) ``` **PHP** ```php $email = $emailit->emails()->send([ 'from' => 'Acme ', 'to' => 'ada@example.com', 'subject' => 'Your appointment is tomorrow', 'text' => 'See you at 14:00.', 'scheduled_at' => '2026-10-05T09:00:00Z', ]); ``` Emailit prepares a scheduled email when you make the request, not at the send time. The template is rendered, URL attachments are downloaded and credits are charged up front. To change the content, cancel the email and send a new one. ## Change the send time Use [Update a scheduled email](/docs/api-reference/emails/update/) (`POST /emails/{id}`) with a new `scheduled_at`. The same formats are accepted. - The email's status must be `scheduled`. - Its current send time must be more than 3 minutes away. - The new send time must be more than 3 minutes in the future. **cURL** ```bash curl https://api.emailit.com/v2/emails/em_33VtK8mRq1xZp7LwN4cY2bHsDfa \ -H "Authorization: Bearer $EMAILIT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "scheduled_at": "2026-10-05T15:00:00Z" }' ``` **Node.js** ```javascript await emailit.emails.update('em_33VtK8mRq1xZp7LwN4cY2bHsDfa', { scheduled_at: '2026-10-05T15:00:00Z', }); ``` **Python** ```python client.emails.update("em_33VtK8mRq1xZp7LwN4cY2bHsDfa", { "scheduled_at": "2026-10-05T15:00:00Z", }) ``` **PHP** ```php $emailit->emails()->update('em_33VtK8mRq1xZp7LwN4cY2bHsDfa', [ 'scheduled_at' => '2026-10-05T15:00:00Z', ]); ``` ```json { "object": "email", "id": "em_33VtK8mRq1xZp7LwN4cY2bHsDfa", "status": "scheduled", "scheduled_at": "2026-10-05T15:00:00.000Z", "updated_at": "2026-10-01T10:02:44.193027Z", "message": "Email schedule has been updated successfully" } ``` Unlike a new send, an unreadable time is rejected here with `422 Invalid scheduled_at`. A request that breaks the 3-minute rule, or targets an email that isn't scheduled, fails with `422 Cannot update email`. A request with several recipients creates one email per recipient, so reschedule each `em_` ID from the `ids` map. Rescheduling isn't available in the dashboard. ## Cancel an email You can cancel an outgoing email while it has one of these statuses: | Status | Can you cancel? | Notes | | --- | --- | --- | | `scheduled` | Yes | Only while the send time is more than 3 minutes away. | | `accepted` | Yes, best effort | The email is waiting in the send queue or about to leave it. | | `attempted` | Yes, best effort | A delivery attempt failed temporarily. Canceling stops the remaining retries. | | Any other status | No | Emails that were delivered, bounced, failed, rejected, suppressed, held, or already canceled can't be canceled. | **Dashboard** 1. Go to **Email API → Emails**. 2. Select **Cancel delivery** on the email's row, or open the email and select **Cancel delivery** at the top of the page. 3. Confirm. If a delivery attempt had already started, the dashboard warns you that the attempt may still complete and that the remaining retries were stopped. **API** Call [Cancel an email](/docs/api-reference/emails/cancel/) (`POST /emails/{id}/cancel`). It works with **Full Access** and **Sending Only** keys. ```bash curl -X POST https://api.emailit.com/v2/emails/em_33VtK8mRq1xZp7LwN4cY2bHsDfa/cancel \ -H "Authorization: Bearer $EMAILIT_API_KEY" ``` ```json { "object": "email", "id": "em_33VtK8mRq1xZp7LwN4cY2bHsDfa", "status": "canceled", "in_flight": false, "message": "Email has been canceled and removed from the send queue." } ``` When `in_flight` is `true`, the email was canceled but a delivery attempt may already be underway, and the message reads "The current delivery attempt may still complete; remaining retries were stopped." A status that can't be canceled, or a scheduled email less than 3 minutes from its send time, returns `422 Cannot cancel email`. Canceling doesn't refund the credits charged when the email was sent through the API. ### How canceling works Canceling pulls the email out of the send queue. It isn't a recall from the recipient's inbox. 1. Emailit checks that the email can still be canceled. 2. It sets the status to `canceled`, adds a "Canceled" entry to the email's delivery history and removes it from the send queue. 3. If a delivery worker has already picked the email up, the worker checks the status again right before handing the message to the recipient's server and skips it when it sees `canceled`. 4. Emailit emits `email.canceled` with the `previous_status`. If the message was already on its way to the recipient's server, that attempt can still succeed. Emailit keeps the status `canceled` even if the racing attempt is delivered or bounces, but the recipient may still get the message. Treat canceling as "stop this before it leaves", not "unsend". ## Statuses and events | Moment | Status | Webhook event | | --- | --- | --- | | Request with a future `scheduled_at` | `scheduled` | [`email.scheduled`](/docs/webhooks/events/email/scheduled/) | | Send time arrives | `delivered`, `attempted`, `bounced` and so on | The matching delivery event, such as [`email.delivered`](/docs/webhooks/events/email/delivered/) | | Canceled | `canceled` | `email.canceled`, with `status` and `previous_status` | A scheduled email doesn't emit `email.accepted` when its send time arrives. See [Email statuses](/docs/logs/email-statuses/) for the full list. ## Related - [Update a scheduled email](/docs/api-reference/emails/update/) - [Cancel an email](/docs/api-reference/emails/cancel/) - [Send an email](/docs/email-api/send-email/) - [Email statuses](/docs/logs/email-statuses/) - [Why is my email stuck in Accepted or Scheduled?](/docs/kb/email-stuck-in-scheduled-or-accepted/) --- Source: https://emailit.com/docs/email-api/scheduling/ --- # Send an email > Send email with POST /emails, covering sender rules, recipients, content, templates, tracking, the response, webhook events and every error code. This guide explains each part of a `POST /emails` request and what Emailit does with it, from the From address to the errors you can get back. For the complete parameter reference, see [Send an email](/docs/api-reference/emails/send/) in the API reference. ## Before you begin - A verified sending domain in your workspace. See [Add a domain](/docs/domains/add-a-domain/). - An API key with **Full Access** or **Sending Only** scope. See [API keys](/docs/developers/api-keys/). - Production access if you send to anyone other than your workspace members. See [Production access](/docs/workspaces/production-access/). - Enough credits for every recipient (1 credit each). ## Send a basic email **cURL** ```bash curl https://api.emailit.com/v2/emails \ -H "Authorization: Bearer $EMAILIT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "from": "Acme Billing ", "to": ["ada@example.com", "Grace Hopper "], "cc": "accounts@example.com", "reply_to": "support@acme.com", "subject": "Your invoice for October", "html": "

Your invoice is ready.

", "text": "Your invoice is ready." }' ``` **Node.js** ```javascript import { Emailit } from '@emailit/node'; const emailit = new Emailit(process.env.EMAILIT_API_KEY); const email = await emailit.emails.send({ from: 'Acme Billing ', to: ['ada@example.com', 'Grace Hopper '], cc: 'accounts@example.com', reply_to: 'support@acme.com', subject: 'Your invoice for October', html: '

Your invoice is ready.

', text: 'Your invoice is ready.', }); ``` **Python** ```python import os from emailit import EmailitClient client = EmailitClient(os.environ["EMAILIT_API_KEY"]) email = client.emails.send({ "from": "Acme Billing ", "to": ["ada@example.com", "Grace Hopper "], "cc": "accounts@example.com", "reply_to": "support@acme.com", "subject": "Your invoice for October", "html": "

Your invoice is ready.

", "text": "Your invoice is ready.", }) ``` **PHP** ```php $emailit = Emailit::client(getenv('EMAILIT_API_KEY')); $email = $emailit->emails()->send([ 'from' => 'Acme Billing ', 'to' => ['ada@example.com', 'Grace Hopper '], 'cc' => 'accounts@example.com', 'reply_to' => 'support@acme.com', 'subject' => 'Your invoice for October', 'html' => '

Your invoice is ready.

', 'text' => 'Your invoice is ready.', ]); ``` ## Set the From address `from` is required and takes one address in either form: - `billing@acme.com` - `Acme Billing `, or with quotes, `"Acme, Inc." ` The domain after the `@` must be a verified sending domain in the same workspace: - **The match is exact.** Domains are compared without regard to case, but `mail.acme.com` and `acme.com` are different domains. Add and verify every subdomain you send from. - **Any local part works.** You don't need a mailbox for `billing@` or `no-reply@`. - **Pending domains can't send.** A domain that is still awaiting review (**Pending verification**) is treated as not verified. - **Restricted keys stay on their domain.** A **Sending Only** key restricted to one domain can only send from that domain. - **Paused domains are blocked.** If [sending health](/docs/deliverability/sending-health/) paused the domain, sends from it are rejected until the pause is lifted. ## Add recipients `to` is required. `cc` and `bcc` are optional. Each field accepts a string or an array of strings, with or without display names, and holds up to 50 addresses. A string can contain several comma-separated addresses; use an array when a display name itself contains a comma. Emailit removes duplicates across `to`, `cc` and `bcc` (ignoring case), then creates **one email per unique recipient**, each with its own `em_` ID. Every copy carries the same `To` and `Cc` headers, so recipients see the conversation as usual, and `Bcc` recipients never appear in any copy's headers. When a request has more than one recipient, the response includes an `ids` map from recipient to email ID. `id` is the email of the first recipient. ```json { "object": "email", "id": "em_33VtK8mRq1xZp7LwN4cY2bHsDfa", "ids": { "ada@example.com": "em_33VtK8mRq1xZp7LwN4cY2bHsDfa", "grace@example.com": "em_33VtK8nB5rTq0YxW3mJk9PdLsUe", "accounts@example.com": "em_33VtK8nQ2wErT6yU8iOp4AsDf7g" } } ``` Each recipient costs 1 credit and counts toward your [rate limits](/docs/api-reference/rate-limits/). A recipient with a `recipient`-type [suppression](/docs/suppressions/) is accepted and then marked `suppressed` instead of being delivered. ## Write the content | Field | Rules | | --- | --- | | `subject` | Required, unless a template provides it. Non-ASCII characters are encoded for you. | | `html` | The HTML body. You need `html`, `text` or both, unless a template provides them. | | `text` | The plain-text body. Send it alongside `html`: some clients and spam filters prefer messages with both. | | `reply_to` | A string or an array of addresses where replies should go. | If `reply_to` names the same address as `from`, Emailit drops the `Reply-To` header because it adds nothing and some spam filters penalize it. ## Send with a template Set `template` to a template alias or a `tem_` ID, and pass `variables` for the [Temple](/docs/templates/temple/) placeholders in it. - **An alias** sends the version that is currently published for that alias. If no version is published, the request fails with `404`. - **A `tem_` ID** sends that exact version, published or not. Use it to test a draft version before you publish it. Fields in the request take precedence over the template: a `subject`, `html` or `text` you send replaces the template's value. If you don't send `reply_to`, the template's Reply-To is used. `from` is always required in the request. See [Template versions](/docs/templates/versions/) for how publishing works. **cURL** ```bash curl https://api.emailit.com/v2/emails \ -H "Authorization: Bearer $EMAILIT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "from": "Acme ", "to": "ada@example.com", "template": "welcome-email", "variables": { "first_name": "Ada", "plan": "Pro", "activation_url": "https://acme.com/activate?token=8f3k2" } }' ``` **Node.js** ```javascript const email = await emailit.emails.send({ from: 'Acme ', to: 'ada@example.com', template: 'welcome-email', variables: { first_name: 'Ada', plan: 'Pro', activation_url: 'https://acme.com/activate?token=8f3k2', }, }); ``` **Python** ```python email = client.emails.send({ "from": "Acme ", "to": "ada@example.com", "template": "welcome-email", "variables": { "first_name": "Ada", "plan": "Pro", "activation_url": "https://acme.com/activate?token=8f3k2", }, }) ``` **PHP** ```php $email = $emailit->emails()->send([ 'from' => 'Acme ', 'to' => 'ada@example.com', 'template' => 'welcome-email', 'variables' => [ 'first_name' => 'Ada', 'plan' => 'Pro', 'activation_url' => 'https://acme.com/activate?token=8f3k2', ], ]); ``` `variables` also works without a template: Emailit renders Temple placeholders in the `subject`, `html` and `text` you send inline. ## Control tracking By default, each email follows its sending domain's **Track loads** and **Track clicks** settings. Override them per email with `tracking`: - `"tracking": true` or `false` turns both load (open) and click tracking on or off. - `"tracking": { "loads": true, "clicks": false }` sets each one separately. Tracking only works when the domain's tracking CNAME is verified. Without it, the email is sent untracked and the request still succeeds. The `tracking` object in the response shows the settings that were actually applied. See [Open and click tracking](/docs/tracking/). ## Add headers and metadata Use `headers` for custom email headers, such as `List-Unsubscribe`, and `meta` for your own string key-value pairs. Emailit stores `meta` with the email and includes it in webhook events. See [Headers and metadata](/docs/email-api/headers-and-metadata/). To attach files, schedule the send, or make retries safe, see [Attachments](/docs/email-api/attachments/), [Scheduling](/docs/email-api/scheduling/) and [Idempotency](/docs/email-api/idempotency/). ## Read the response A successful request returns `200`: | Field | Description | | --- | --- | | `object` | Always `email`. | | `id` | The `em_` ID of the first recipient's email. | | `ids` | Map of recipient address to email ID. Present only when there is more than one recipient. | | `token` | Internal token of the first email, also used in its Message-ID. | | `message_id` | The `Message-ID` header of the first email, in the form ``. | | `from` | The From address as you sent it. | | `to` | The `to` addresses, without display names. | | `cc`, `bcc` | The `cc` and `bcc` addresses. Present only when you sent them. | | `subject` | The final subject, after template rendering. | | `status` | `accepted`, or `scheduled` when the email has a future send time. | | `scheduled_at` | The send time in ISO 8601, or `null`. | | `created_at` | When the email was created. | | `tracking` | The applied `loads` and `clicks` settings. | Store the `id` (or the `ids` map) so you can match later webhook events and look the email up with [Retrieve an email](/docs/api-reference/emails/get/). ## Events Every recipient's email emits its own events: 1. [`email.accepted`](/docs/webhooks/events/email/accepted/) right after the request, or [`email.scheduled`](/docs/webhooks/events/email/scheduled/) if it has a future send time. 2. Delivery events as the email moves through the pipeline: [`email.delivered`](/docs/webhooks/events/email/delivered/), [`email.attempted`](/docs/webhooks/events/email/attempted/) (temporary failure, will retry), [`email.bounced`](/docs/webhooks/events/email/bounced/), [`email.failed`](/docs/webhooks/events/email/failed/), [`email.rejected`](/docs/webhooks/events/email/rejected/) or [`email.suppressed`](/docs/webhooks/events/email/suppressed/). An email held for review emits `email.held`. 3. Engagement events, if tracking is on: [`email.loaded`](/docs/webhooks/events/email/loaded/) and [`email.clicked`](/docs/webhooks/events/email/clicked/). Spam reports emit [`email.complained`](/docs/webhooks/events/email/complained/). See [Email statuses](/docs/logs/email-statuses/) for what each status means. ## Errors Validation errors return a list of every problem found: ```json { "error": "Validation failed", "validation_errors": [ "Missing required field: subject", "Invalid to email address at index 1: grace@" ] } ``` | Status | `error` | Cause | Fix | | --- | --- | --- | --- | | `400` | `Validation failed` | A required field is missing, an address is malformed, a field has more than 50 recipients, or an attachment is invalid. | Fix each item in `validation_errors`. | | `400` | `Invalid Idempotency-Key` | The `Idempotency-Key` header has a bad format. | Use 1–256 letters, digits, `-` or `_`. See [Idempotency](/docs/email-api/idempotency/). | | `401` | `Unauthorized` | The API key is missing or invalid. | Send `Authorization: Bearer` with a current key. | | `402` | `Insufficient credits` | The workspace can't pay for every recipient. | [Buy credits](/docs/billing/credits/) or turn on [auto-refill](/docs/billing/auto-refill/). | | `403` | `Workspace not verified` | The workspace is in sandbox mode and a recipient isn't a workspace member. `code` is `unverified_workspace_recipient` and `blocked_recipients` lists the addresses. | [Request production access](/docs/workspaces/production-access/), or test with members' addresses. | | `403` | `Domain not authorized` | The API key is restricted to a different sending domain. | Send from the key's domain, or use a key without a domain restriction. | | `403` | `Domain paused` | Sending health paused the From domain. | See [Sending health](/docs/deliverability/sending-health/). | | `404` | `Template not found` | The alias has no published version, or the `tem_` ID doesn't exist in this workspace. | Publish a version or check the ID. | | `409` | `Idempotency key in progress` | Another request with the same key is still running. | Wait, then retry with the same key. | | `413` | `Message too large` | The encoded message is larger than 40 MB. | Send fewer or smaller attachments, or link to large files. | | `422` | `Domain not verified` | The From domain isn't a verified sending domain in this workspace. | Verify the domain, or check for a subdomain or typo. | | `422` | `Attachment error` | An attachment URL couldn't be downloaded or is larger than 25 MB. | See [Attachments](/docs/email-api/attachments/). | | `429` | `Rate limit exceeded` or `Daily limit exceeded` | You're over the per-second or daily sending limit. | Wait for `retry-after` seconds, or request a higher limit. | | `503` | `Idempotency unavailable` | The idempotency store couldn't be reached. | Retry with the same key. | A suspended workspace gets `403` with `Workspace is suspended` on every send. For the general error format, see [Errors](/docs/api-reference/errors/). ## Related - [Send an email](/docs/api-reference/emails/send/) API reference - [Templates](/docs/templates/) - [Email statuses](/docs/logs/email-statuses/) - [Webhook event types](/docs/webhooks/event-types/) - [Why didn't my email arrive?](/docs/kb/email-not-delivered-checklist/) --- Source: https://emailit.com/docs/email-api/send-email/ --- # Email verification > Check whether email addresses are valid and safe to mail before you send, one at a time or in lists of up to 10,000, with a result, risk level and 0–100 score. Email verification checks an address before you send to it, so you can avoid bounces and keep your sending health high. Emailit tells you whether each address is safe to mail, how risky it is and why. You can verify single addresses, for example at sign-up, or whole lists before an import or campaign. ## How it works Emailit runs a series of checks on each address: 1. **Syntax.** Is it a correctly formed email address? 2. **Domain.** Does the domain have mail (MX) servers, and how old is it? 3. **Address type.** Is it a disposable, role (such as `info@`), free-provider or random-looking address? 4. **Mailbox** (full mode only). Emailit connects to the recipient's mail server and asks whether it would accept the mailbox, without sending a message. This detects mailboxes that don't exist, are disabled or full, and domains that accept every address (catch-all). The checks combine into three answers: | Answer | Values | Use it to | | --- | --- | --- | | **Result** | `safe`, `invalid`, `disposable`, `disabled`, `inbox_full`, `role`, `unknown` | Decide what to do with the address. | | **Risk** | `low`, `medium`, `high` | Sort addresses by how likely they are to cause problems. | | **Score** | 0 to 100 | Rank addresses or set your own threshold. | [Verification results](/docs/email-verification/results/) explains every value, how the score is calculated and what to do with each result. ## Single addresses and lists | | Single verification | Verification list | | --- | --- | --- | | Addresses | One per request | Up to 10,000 unique addresses per list | | Speed | Answer in the API response | Processed in the background | | Mode | `fast` (default) or `full` | Always `full` | | Where | Dashboard or API | Dashboard (paste or upload CSV or XLSX) or API | | Export | – | XLSX file of all results | `fast` mode skips the mailbox check, so it answers quickly and suits sign-up forms. `full` mode adds the mailbox check for a more reliable answer. ## Credits Each verified address costs 5 credits, from your workspace's included credits first and then purchased credits. Lists are charged up front for every unique address. If the workspace doesn't have enough credits, the request returns `402`. | Action | Credits | | --- | --- | | Email sent with the API or SMTP (per recipient) | 1 | | Inbound email received | 1 | | Campaign email (per recipient) | 2 | | Automation run | 3 | | Email verification (per address) | 5 | At the extra credit prices, that's $1 per 1,000 verifications on Pay as you go and $0.50 per 1,000 on Pro and Business. See [Credits](/docs/billing/credits/). ## Results expire after 30 days Verifications and lists are deleted 30 days after you create them. The **Expiring** column in the dashboard shows how many days are left. Export or store any results you want to keep. An address can change in 30 days anyway, so verify again before a big send. ## Availability Email verification is available on every plan. AI assistants can verify addresses and lists through the [MCP server](/docs/mcp/tools/#toolset-verification) too. ## Get started - [Verify an address](/docs/email-verification/single/): Check one address in the dashboard or with the API. - [Verify a list](/docs/email-verification/lists/): Upload up to 10,000 addresses and export the results. - [Verification results](/docs/email-verification/results/): Every result, risk level, check and recommended action. - [Verification API](/docs/api-reference/email-verifications/): Verify addresses from your code. --- Source: https://emailit.com/docs/email-verification/ --- # Verify a list > Verify up to 10,000 email addresses at once by pasting them or uploading a CSV or XLSX file, follow progress, review results and export them as XLSX. A verification list checks many addresses in one go, for example before you import contacts from another system or send a campaign to an old list. This guide shows how to create a list, follow its progress, read the results and export them. ## Before you begin - A list can contain up to 10,000 addresses. Split bigger files into several lists. - Each unique address costs 5 credits, charged when you create the list. A list of 10,000 addresses needs 50,000 credits. - Lists always use `full` mode, which includes the mailbox check. See [Fast and full mode](/docs/email-verification/single/#fast-and-full-mode). - For the API, use an API key with **Full Access**. ## Create a list **Dashboard** 1. **Open Lists.** Go to **Email Verification → Lists** and select **Verify emails**. 2. **Name the list.** In **Name**, enter something you'll recognize later, such as `Newsletter import October`. 3. **Add the addresses.** Under **Input Method**, choose one: - **Enter emails manually:** paste addresses into **Email Addresses**, separated by commas, semicolons or new lines. - **Upload a file:** choose a `.csv` or `.xlsx` file smaller than 10 MB. In a CSV, put one address per row or separate them with commas. In Excel, put them in the first column, one per row. The dialog shows how many addresses it found. 4. **Select Create.** Emailit charges the credits and starts verifying. **API** Call [Create a list](/docs/api-reference/email-verifications/lists/create/) with a name and an array of addresses: ```bash curl https://api.emailit.com/v2/email-verification-lists \ -X POST \ -H "Authorization: Bearer $EMAILIT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Newsletter import October", "emails": ["ada@example.com", "grace@example.com", "alan@example.com"] }' ``` The `201` response describes the new list: ```json { "object": "email_verification_list", "id": "evl_3Rt8Wq5Lm2Kx", "name": "Newsletter import October", "status": "processing", "stats": { "total_emails": 3, "processed_emails": 0, "successful_verifications": 0, "failed_verifications": 0, "pending_emails": 3 }, "created_at": "2026-10-01T11:02:37Z", "updated_at": "2026-10-01T11:02:37Z", "valid_emails_count": 3, "unique_emails_count": 3, "invalid_emails_count": 0, "dispatched_jobs": 3 } ``` | Status | When | | --- | --- | | `201` | The list was created and verification started. | | `400` | `name` is missing, `emails` is empty, it has more than 10,000 items, or none of them looks like an email address. | | `402` | The workspace doesn't have enough credits for every unique address. | Before charging, Emailit cleans up the input: it lowercases every address, removes duplicates and drops entries without an `@`. You're only charged for the unique addresses that remain. If the list can't be started, the credits are refunded. ## Follow progress The list's status moves through these stages: | Status | Meaning | | --- | --- | | `pending` | Created, waiting to start. | | `processing` | Addresses are being verified. | | `completed` | Every address has a result. | | `failed` | The list couldn't be processed. | | `canceled` | Verification was stopped. | In **Email Verification → Lists**, the **Statistics** column shows **Total**, **Pending**, **Successful** and **Failed** counts for each list. The **Expiring** column shows how many days are left before the list is deleted. With the API, call [Retrieve a list](/docs/api-reference/email-verifications/lists/get/) and read `status` and `stats`. To avoid polling, subscribe a [webhook](/docs/webhooks/set-up/) to `email_verification_list.updated`, which fires when the list completes. Each address also fires `email_verification.updated` when its result is ready. ## Review the results Select a list in the dashboard to open it. The page shows: - cards with **Created**, **Status**, **Total Emails**, **Pending**, **Successful** and **Failed**, - charts for **Verification Status Progress**, **Verification Results Breakdown** and **Risk Levels**, - a table of every address with its status, result, risk and score. With the API, call [List results](/docs/api-reference/email-verifications/lists/results/). Filter by `result` or `status` and page through with `page` and `limit` (default 50): ```bash curl -G https://api.emailit.com/v2/email-verification-lists/evl_3Rt8Wq5Lm2Kx/results \ -H "Authorization: Bearer $EMAILIT_API_KEY" \ --data-urlencode "result=invalid" \ --data-urlencode "limit=100" ``` Each item has the address's `id`, `email`, `status`, `result`, `score`, `risk` and `mx_records`. [Verification results](/docs/email-verification/results/) explains every value and what to do with it. ## Export the results Once a list is `completed`, you can download all its results as an Excel file. **Dashboard** Open the list and select **Export Results**. Emailit downloads an `.xlsx` file named after the list's ID. **API** Call [Export results](/docs/api-reference/email-verifications/lists/export/) and save the response: ```bash curl https://api.emailit.com/v2/email-verification-lists/evl_3Rt8Wq5Lm2Kx/export \ -H "Authorization: Bearer $EMAILIT_API_KEY" \ -o results.xlsx ``` Exporting a list that isn't completed returns `400`. The file has one row per address with its status, result, score and risk, every individual check (valid syntax, disposable, role account, inbox full, deliverable, disabled, free email, gibberish, catch-all, SMTP connect, MX records), the address parts, domain age, the `did_you_mean` suggestion and the MX records. Results are color-coded so you can scan them quickly. > **Lists are deleted after 30 days:** Verification lists and their results are deleted 30 days after you create them. Export the file if you need to keep the results longer. ## Use the results 1. **Remove** addresses with the result `invalid`, `disabled` or `disposable` before you import or send. 2. **Review** `inbox_full`, `role` and `unknown` addresses. Keep them if you have a reason to, but watch their bounces. 3. **Import** the rest into your [contacts](/docs/contacts/import-export/) or audience. See [Recommended actions](/docs/email-verification/results/#recommended-actions) for each result. ## Related - [Verification results](/docs/email-verification/results/): Every result, risk level and check. - [Verification lists API](/docs/api-reference/email-verifications/lists/): Create lists and read results from code. --- Source: https://emailit.com/docs/email-verification/lists/ --- # Verification results > Reference for every email verification result, risk level and check, how the 0–100 score is calculated, and what to do with each kind of address. Every verification returns a result, a risk level, a score and the individual checks behind them. This page explains each value so you can decide which addresses to keep, review or remove. The same fields appear in the dashboard, the API and list exports. ## Results Emailit picks the first result in this table that applies, from top to bottom: | Result | Dashboard label | When | Mode | | --- | --- | --- | --- | | `invalid` | Invalid | The address isn't correctly formed, or its domain has no MX records, so it can't receive mail. | Both | | `disposable` | Disposable | The domain belongs to a temporary or throwaway email service. | Both | | `disabled` | Disabled | The recipient's server said the mailbox is disabled or inactive. | `full` | | `inbox_full` | Inbox Full | The recipient's server said the mailbox is full or over its quota. | `full` | | `role` | Role | The address belongs to a role or team rather than a person, such as `info@`, `support@` or `sales@`. | Both | | `safe` | Safe | None of the above, and the score is 70 or higher. | Both | | `unknown` | Unknown | None of the above, and the score is below 70. Several weaker signals add up, for example a random-looking mailbox on a free provider. | Both | `disabled` and `inbox_full` only come from `full` mode, because they need the mailbox check. The dashboard's **Result** filter also lists **Valid**, **Catch All** and **Risky**. Current verifications don't return these values. Catch-all domains are reported by the `catch_all` check instead, and they lower the score. ## Risk | Risk | When | | --- | --- | | `high` | The address has invalid syntax, no MX records, a disposable domain or a disabled mailbox, or its score is below 50. | | `medium` | Score from 50 to 79. | | `low` | Score of 80 or more. | The dashboard's **Risk** filter also offers **Unknown**, which current verifications don't return. ## Score The score runs from 0 (certain to fail) to 100 (very likely good). Emailit starts at 100, applies these adjustments and keeps the result between 0 and 100: | Condition | Change | | --- | --- | | Invalid syntax | −50 | | No MX records | −40 | | Disabled mailbox (`full` mode) | −40 | | Disposable domain | −30 | | Random-looking mailbox (`gibberish`) | −25 | | Mailbox not deliverable (`full` mode) | −20 | | Role account | −15 | | Inbox full (`full` mode) | −15 | | Catch-all domain (`full` mode) | −10 | | Free email provider | −5 | | Has MX records | +5 | | Domain at least 30 days old | +10 | An address that can't be split into a mailbox and a domain gets a score of 0. Examples: | Address | Calculation | Score | Result | Risk | | --- | --- | --- | --- | --- | | `ada@gmail.com` | 100 − 5 (free) + 5 (MX) + 10 (old domain) | 100 | `safe` | `low` | | `info@acme.com` | 100 − 15 (role) + 5 + 10 | 100 | `role` | `low` | | `x7kq2vz9@gmail.com` | 100 − 25 (gibberish) − 5 (free) + 5 + 10 | 85 | `safe` | `low` | | `ada@mailinator.com` | 100 − 30 (disposable) + 5 + 10 | 85 | `disposable` | `high` | | `ada@acme.invalid` | 100 − 40 (no MX) | 60 | `invalid` | `high` | In the dashboard, the score bar is green at 80 and above and shifts toward red as the score drops. ## Checks Each verification includes a `checks` object. In `fast` mode, the checks marked `full` are `null` because they weren't tested. | Check | Type | Mode | Meaning | | --- | --- | --- | --- | | `valid_syntax` | boolean | Both | The address follows email syntax rules, including international characters. | | `has_mx_records` | boolean | Both | The domain publishes MX records, so it can receive mail. | | `domain_age` | integer or null | Both | Age of the domain in days, from WHOIS. `null` when it can't be determined. | | `disposable` | boolean | Both | The domain is a known temporary or throwaway email service. | | `role_account` | boolean | Both | The mailbox is a role address, such as `info`, `admin` or `support`. | | `free_email` | boolean | Both | The domain is a free email provider, such as Gmail or Yahoo. | | `gibberish` | boolean | Both | The mailbox looks randomly generated. | | `smtp_connect` | boolean or null | `full` | Emailit connected to the domain's mail server. | | `deliverable` | boolean or null | `full` | The server accepted the mailbox, or answered in a way that suggests it exists. `false` means the server rejected the mailbox as unknown. | | `disabled` | boolean or null | `full` | The server said the mailbox is disabled or inactive. | | `inbox_full` | boolean or null | `full` | The server said the mailbox is full or over quota. | | `catch_all` | boolean or null | `full` | The domain accepts mail for any address, so the specific mailbox can't be confirmed. | Some large mailbox providers don't answer mailbox checks reliably. For their addresses, `full` mode may not add much over `fast`. ## Other fields | Field | Description | | --- | --- | | `id` | Starts with `ev_`. | | `email` | The address you verified. | | `status` | `pending`, `processing`, `completed` or `failed`. Single verifications return `completed`. List items can also be `failed`, with an `error_message`. | | `mode` | `fast` or `full`. | | `address.mailbox` | The part before `@`, without any `+` tag. For `ada+news@acme.com`, that's `ada`. | | `address.domain` | The part after `@`. | | `address.suffix` | The `+` tag, such as `news`, or `null`. | | `address.root` | The address without the `+` tag, such as `ada@acme.com`. | | `did_you_mean` | A suggested correction for a likely typo in the domain, such as `ada@gmail.com` for `ada@gmial.com`, or `null`. | | `mx_records` | The domain's MX records, each with `priority` and `exchange`, sorted by priority. | | `created_at`, `updated_at` | Timestamps. Results are deleted 30 days after `created_at`. | ## Recommended actions | Result | What to do | | --- | --- | | `safe` | Send. | | `invalid` | Remove the address. At sign-up, ask the person to correct it, and show `did_you_mean` if there is one. | | `disposable` | Don't add it to marketing lists. At sign-up, ask for a permanent address. | | `disabled` | Remove the address. The mailbox no longer accepts mail. | | `inbox_full` | Pause sending and verify again later. Remove it if it stays full. | | `role` | Fine for transactional and business mail. For marketing, send only if the address opted in, and watch complaints. | | `unknown` | Send with care, or verify again in `full` mode. Leave these out of the first sends on a new domain. | Two checks are worth acting on regardless of the result: - **`deliverable: false`** means the recipient's server rejected the mailbox. Treat the address like `invalid`, even if its result is `safe`. - **`catch_all: true`** means the domain accepts everything, so a bounce can still happen later. Send, but keep an eye on bounces from that domain. ## Related - [Verify a single address](/docs/email-verification/single/): Verify in the dashboard or with the API. - [Verify a list](/docs/email-verification/lists/): Check up to 10,000 addresses and export them. - [Bounces and complaints](/docs/deliverability/bounces-and-complaints/): What happens when an address bounces. --- Source: https://emailit.com/docs/email-verification/results/ --- # Verify a single address > Verify one email address in the dashboard or with the API, choose between fast and full mode, and use verification to catch bad addresses at sign-up. This guide shows how to check one email address, either by hand in the dashboard or from your code with the API. It also covers using verification in a sign-up form to stop typos and throwaway addresses before they reach your list. ## Before you begin - Each verification costs 5 credits. Check your balance in the sidebar or in **Workspace → Billing**. - For the API, use an API key with **Full Access**. Sending-only keys can't verify addresses. ## Fast and full mode | Mode | Checks | Use it for | | --- | --- | --- | | `fast` (default) | Syntax, MX records, domain age, disposable, role, free-provider and random-looking addresses | Sign-up forms and anything a person is waiting on. | | `full` | Everything in `fast`, plus a connection to the recipient's mail server to check the mailbox: deliverable, disabled, inbox full and catch-all | Cleaning addresses before an important send, when a few extra seconds don't matter. | Both modes cost the same. `full` takes longer because it talks to the recipient's mail server, which can be slow or refuse to answer. Some large providers block these checks, so `full` can't always confirm a mailbox. See [Verification results](/docs/email-verification/results/) for what each check means. ## Verify an address **Dashboard** 1. **Open Email Verification.** Go to **Email Verification → Emails** and select **Verify Email**. 2. **Enter the address.** Type it into **Email** and select **Verify Email**. The dashboard uses `fast` mode. 3. **Open the result.** The address appears in the list with its **Status**, **Result**, **Risk**, **Score**, **Created** date and days until it's **Expiring**. Select it to see every individual check, such as **Valid Syntax**, **Disposable**, **Role Account** and **Has MX Records**. To check many addresses at once, [verify a list](/docs/email-verification/lists/) instead. **API** Call [Verify an address](/docs/api-reference/email-verifications/verify/) with the address and, optionally, the mode: ```bash curl https://api.emailit.com/v2/email-verifications \ -X POST \ -H "Authorization: Bearer $EMAILIT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "email": "ada@example.com", "mode": "full" }' ``` The result comes back in the response: ```json { "id": "ev_6Hq2Lm9Tx4Pz", "object": "email_verification", "email": "ada@example.com", "status": "completed", "score": 100, "risk": "low", "result": "safe", "mode": "full", "checks": { "valid_syntax": true, "disposable": false, "role_account": false, "inbox_full": false, "deliverable": true, "disabled": false, "free_email": false, "gibberish": false, "catch_all": false, "smtp_connect": true, "has_mx_records": true, "domain_age": 10957 }, "address": { "mailbox": "ada", "domain": "example.com", "suffix": null, "root": "ada@example.com" }, "did_you_mean": null, "mx_records": [ { "priority": 10, "exchange": "mx1.example.com" } ], "created_at": "2026-10-01T10:24:11Z", "updated_at": "2026-10-01T10:24:11Z" } ``` In `fast` mode, the mailbox checks (`deliverable`, `disabled`, `inbox_full`, `catch_all` and `smtp_connect`) are `null` because they weren't tested. | Status | When | | --- | --- | | `200` | The address was verified. This includes badly formed addresses, which come back with `result: "invalid"`. | | `400` | `email` is missing, or `mode` isn't `fast` or `full`. | | `402` | The workspace has fewer than 5 credits. | Every successful verification also fires an `email_verification.created` event. See [Webhook event types](/docs/webhooks/event-types/). ## Verify addresses at sign-up Checking an address when someone types it is the cheapest way to keep a list clean. A typo caught at sign-up never becomes a bounce. 1. **Call the API from your server.** Never put your API key in browser code. Send the address from your sign-up handler, in `fast` mode. 2. **Set a timeout and fail open.** If verification is slow or returns an error, let the sign-up through. Losing a real customer costs more than one bad address. 3. **Act on the result.** Block what's clearly wrong, suggest fixes for typos and let everything else through. ```javascript title="verify-signup.js" const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), 5000); try { const res = await fetch('https://api.emailit.com/v2/email-verifications', { method: 'POST', headers: { Authorization: `Bearer ${process.env.EMAILIT_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ email, mode: 'fast' }), signal: controller.signal, }); if (!res.ok) return { ok: true }; // fail open const verification = await res.json(); if (verification.result === 'invalid') { return { ok: false, message: verification.did_you_mean ? `Did you mean ${verification.did_you_mean}?` : 'Please check your email address.', }; } if (verification.result === 'disposable') { return { ok: false, message: 'Please use a permanent email address.' }; } // Accept the address, but offer a correction if one looks likely. return { ok: true, suggestion: verification.did_you_mean }; } catch { return { ok: true }; // timeout or network error: fail open } finally { clearTimeout(timer); } } ``` A few tips: - **Use `did_you_mean`.** For common domain typos, such as `ada@gmial.com`, it contains the likely address, `ada@gmail.com`. Show it as a suggestion the person can accept. - **Don't block role addresses outright.** `info@` or `sales@` is often how small businesses sign up. Flag them instead. - **Verify once per address.** Each call costs credits, so verify when the form is submitted, not on every keystroke, and store the result. ## Related - [Verification results](/docs/email-verification/results/): What every result, risk and check means. - [Verify a list](/docs/email-verification/lists/): Check up to 10,000 addresses at once. --- Source: https://emailit.com/docs/email-verification/single/ --- # Build a form > Use the form builder to add steps, text, images, buttons and input fields, style the form, preview it on each device and publish it once the checks pass. The form builder is a full-screen editor where you design what visitors see: one or more form steps, a success step, every block on them and the form's styles. This page walks through each part of the builder and the checks a form must pass before you can publish it. ## Before you begin - Forms is in early access. See [Forms](/docs/forms/) if it isn't available in your workspace. - Create the form under **Email Marketing → Forms** with **Create form**, giving it a **Name** and a **Type**. ## Open the builder On the form's page, select **Edit form**. The builder opens over the dashboard: - **Header:** the form name (select it to rename), the **Draft** or **Live** badge, an **Unsaved** marker when you have changes, **Undo** and **Redo**, one button per step, **Add step**, **Publish**, **Save** and the close button. - **Canvas:** a live preview of the selected step. Switch between **Desktop**, **Mobile** and **Both**, check the form's size, zoom with **Scale** and select **Reset canvas** to re-center it. - **Side panel:** the **Content** and **Styles** tabs, and **Alerts & checks** at the bottom. Select **Save** to keep your changes. Saving a live form updates it on your site too. ## Steps A new form has two steps: **Step 1**, with a "Join our list" text, an **Email** field and a **Subscribe** button, and a **Success** step that says "Thanks for subscribing!". - **Form steps** hold the fields visitors fill in. Select **Add step** (the plus button in the header) to add another form step. New steps go before the success step. - **The success step** is shown after the visitor submits the form. Use it to say thanks, link somewhere or offer a discount code. - **Move between steps** with buttons. A **Next step** button validates the current step and shows the next one. A **Submit** button validates the step, sends the submission and shows the success step. Select a step's button in the header to edit it on the canvas. ## Add blocks On the **Content** tab, select a block to add it to the current step. To insert a block in a specific place, hover over the canvas between two blocks and select the plus button. Drag blocks to reorder them, and select a block to edit its settings. **Delete** is at the top of each block's settings. | Group | Block | Use it for | | --- | --- | --- | | **Elements** | **Text** | Headings and paragraphs, with bold, italic, links and emoji. | | | **Button** | Submitting, moving to the next step, closing the form or opening a link. | | | **Image** | A logo, product shot or banner image, optionally linked. | | **Inputs** | **Email** | The visitor's email address. Checked for a valid format. | | | **Text input** | Any short text, such as a name or company. | | | **Phone** | A phone number. Checked for a valid format. | | | **Radio** | One choice from a list. | | | **Checkbox** | Several choices from a list, or a single consent checkbox. | | | **Date** | A date, in the format you choose. | | | **Dropdown** | One choice from a list, in a compact menu. | ### Block settings **Text** - Content, edited inline. - **Typography:** **Alignment**, **Font size**, **Weight**, **Color** and **Link color**. **Button** - **Label** and **Action**: **Submit**, **Next step**, **Close** or **Go to URL**. **Go to URL** also needs a **URL**. - **Button style:** **Width** (auto or full), **Alignment**, **Background**, **Text**, **Hover** and **Border radius**. **Image** - **Image URL**, **Alt text** and an optional **Link URL**. - **Layout:** **Width (%)**, **Alignment** and **Object fit**. **Email, Text input, Phone and Date** - **Property name:** the key the value is stored under in each submission, for example `email` or `company`. Keep it unique on the form. - **Label**, **Placeholder** (or **Date format** for dates: `MM/DD/YYYY`, `DD/MM/YYYY` or `YYYY-MM-DD`) and **Help text**. - **Required**, with an optional **Required message**, and an **Invalid message** shown when an email address or phone number doesn't look valid. Phone numbers are easiest to validate in international format, such as `+15551234567`. **Radio, Checkbox and Dropdown** - **Property name**, **Label** and **Help text**. - **Required**, with an optional **Required message**. - **Layout:** vertical or horizontal. - **Options**, one per line as `label|value`, for example `Weekly digest|weekly`. **Every block** - **Spacing:** **Margin top** and **Margin bottom**. - **Visibility:** **Show on** **Desktop & mobile**, **Desktop only** or **Mobile only**. Hidden blocks stay in the form and are only filtered out of the matching preview and device. ## Style the form The **Styles** tab controls the whole form: | Section | Settings | | --- | --- | | **Form type** | **Type**. For flyouts, **Flyout side** (**Bottom right**, **Bottom left** or **Bottom**). For banners, **Banner position** (**Top** or **Bottom**). | | **Size** | **Width (px)** and **Min height (px)**. Leave the height at 0 for automatic height. | | **Overlay** | **Overlay color** and **Opacity** behind popups, flyouts and full-page forms. | | **Form background** | **Fill**, **Border color**, **Border width**, **Border radius**, **Padding** and **Shadow**. | | **Side image** | An image to the **Left** or **Right** of the form, with **Image URL**, **Alt text** and **Size**. | | **Background image** | An image behind the form, with **Image URL** and **Fit** (**Cover** or **Contain**). | | **Fonts** | **Font family** (**Inter**, **Georgia**, **Mono** or **System**), **Heading color** and **Body color**. | | **Input field styles** | **Background**, **Border**, **Border radius**, **Text**, **Placeholder** and **Label** colors. | | **Button styles** | **Fill**, **Text**, **Hover**, **Border radius** and **Style** (**Solid** or **Outline**). Individual buttons can override these. | | **Close icon** | **Color**, **Size** and **Style** (**X** or **Circle X**) of the close button. Not shown for embedded forms. | ## Preview Use **Desktop**, **Mobile** or **Both** above the canvas to check the layout on each screen size, including blocks you've hidden on one device. **Undo** and **Redo** step through your recent changes. ## Check and publish **Alerts & checks** at the bottom of the side panel lists problems with the form. A red count shows how many errors there are. | Level | Check | | --- | --- | | Error | "Add at least one form step before publishing." | | Error | "Add a success step before publishing." | | Warning | A form step has no content blocks. | | Warning | More than one email field. Use a single email field when possible. | | Warning | No submit or next button on any form step. | | Warning | A **Go to URL** button has no URL. | Errors block publishing. Warnings don't, but fix them so visitors can complete the form. When the checks pass, select **Publish** in the builder header. Emailit saves the form, makes it live and closes the builder. You can also publish or unpublish from the form's page. Next, [install the script](/docs/forms/install/) on your site if you haven't yet. ## Related - [Install forms](/docs/forms/install/): Show your live forms on your site. - [Forms overview](/docs/forms/): Form types, statuses and submissions. --- Source: https://emailit.com/docs/forms/build/ --- # Forms > Build popup, flyout, banner, full-page and embedded sign-up forms in the dashboard, publish them to your site with one script tag, and collect submissions. Forms are sign-up forms you design in the Emailit dashboard and show on your website. You build a form visually, publish it, and add one script tag to your site, and Emailit stores every submission for you. > **Forms is in early access:** Forms isn't available in every workspace yet. If **Forms** shows a **Soon** badge in your dashboard sidebar and can't be opened, it isn't enabled for your workspace. [Contact support](/contact/) if you'd like early access. ## How it works 1. **Create a form** under **Email Marketing → Forms** and pick its type. 2. **Design it** in the builder: add text, images, buttons and input fields, split it into steps and style it. See [Build a form](/docs/forms/build/). 3. **Publish it.** Only live forms appear on your site. 4. **Install the script** once on every page of your site. Popups, flyouts, banners and full-page forms then appear by themselves. Embedded forms also need a placeholder where they should render. See [Install forms](/docs/forms/install/). 5. **Review submissions** on the form's page. ## Form types | Type | How it appears | | --- | --- | | **Popup** | A dialog in the middle of the page over a dimmed overlay. Opens a few seconds after the page loads. | | **Full page** | Covers the whole page. Opens a few seconds after the page loads. | | **Flyout** | Slides in at the bottom right, bottom left or bottom of the page. Opens a few seconds after the page loads. | | **Banner** | A bar across the top or bottom of the page. Shows as soon as the page loads. | | **Embed** | Renders inline, inside your page content, wherever you place its placeholder. | You pick the type when you create the form and can change it later in the builder's **Styles** panel. Each page load shows at most one popup, flyout or full-page form, plus any live banners and embeds. ## Statuses | Status | Meaning | | --- | --- | | **Draft** | The form is saved but not shown on your site. New forms start as drafts. | | **Live** | The form is published and shows wherever the script is installed. | To publish, a form needs at least one form step and a success step. **Unpublish** returns a live form to draft and removes it from your site. ## Submissions Every time a visitor submits a form, Emailit stores the submission with the value of each field, keyed by the field's property name, and details about where it came from, such as the page URL and referrer. The form's page lists them under **Recent submissions**, newest first. Submissions are stored with the form only. They don't create contacts or add anyone to an audience yet, so a form doesn't grow your audiences by itself. To add sign-ups to an audience today, use an audience's [subscribe URL](/docs/audiences/subscribe-url/) or the [Subscribers API](/docs/api-reference/audiences/subscribers/). Deleting a form permanently deletes its submissions too. ## The Forms list **Email Marketing → Forms** lists your forms with their **Name**, **Type**, **Status** and **Updated** time, with search. - **Create form** asks for a **Name** and a **Type**, then opens the new form. - **Install** shows the script tag for your site. - The row menu has **Edit**, **Rename** and **Delete**. On a form's page you'll find **Publish** or **Unpublish**, **Edit form** to open the builder, a **Details** card with the number of steps and the form's **Public token**, the **Recent submissions** table and an **Installation** card with the code for this form. ## Use the API The [Forms API](/docs/api-reference/forms/) lets you [create](/docs/api-reference/forms/create/), [retrieve](/docs/api-reference/forms/get/), [update](/docs/api-reference/forms/update/), [list](/docs/api-reference/forms/list/) and [delete](/docs/api-reference/forms/delete/) forms, [publish](/docs/api-reference/forms/publish/) and [unpublish](/docs/api-reference/forms/unpublish/) them, and [reset the public token](/docs/api-reference/forms/reset-token/). Forms have `frm_` IDs, a `type` (`popup`, `full_page`, `flyout`, `embed` or `banner`), a `status` (`draft` or `live`) and the full builder `definition`. Publishing a form without a form step and a success step returns `422`. Submissions are available in the dashboard only. ## Next steps - [Build a form](/docs/forms/build/): Use the builder to design steps, fields and styles. - [Install forms](/docs/forms/install/): Add the script to your site and place embedded forms. --- Source: https://emailit.com/docs/forms/ --- # Install forms on your site > Add the Emailit script to your site once, place embedded forms, open forms from your own code, and manage form tokens, publishing and submissions. One script tag shows all of your workspace's live forms on your site. This page explains where to put it, how to place embedded forms, how to open a form from your own code and how to check that submissions arrive. ## Before you begin - Build and publish at least one form. Only live forms appear on your site. See [Build a form](/docs/forms/build/). - You need to be able to edit your site's HTML, or add a custom HTML tag in your tag manager. ## Add the script 1. **Copy the snippet.** On **Email Marketing → Forms**, select **Install**. The snippet is also on every form's page, in the **Installation** card. It looks like this, with your workspace's public key in the path: ```html ``` 2. **Add it to every page.** Paste it once into the shared layout or template of your site, ideally just before the closing `` tag. It's the same snippet for all forms in the workspace, so you only install it once. 3. **Publish your forms.** Live popups, flyouts, full-page forms and banners now appear on your site without more code. The script loads asynchronously and doesn't block your page. If anything fails, such as a network error, it stays silent and your page keeps working. It also works when added through a tag manager. If your site uses a Content Security Policy, allow `https://js.emailit.com` in `script-src` and `connect-src`. ## How overlay forms appear | Type | When it shows | | --- | --- | | **Banner** | As soon as the page loads. Every live banner is shown. | | **Popup**, **Flyout**, **Full page** | 2.5 seconds after the page loads. Only one of these shows per page load. | To change the delay, set `display_delay_seconds` in the form's `settings` with [Update a form](/docs/api-reference/forms/update/). Once a visitor closes a form, it stays hidden in that browser for 7 days. After they submit it, it stays hidden for a year. Emailit remembers this in the browser's local storage, under the key `emailit_forms_v1`, and sets no cookies. If local storage isn't available, forms show on every page load. ## Embed a form in your page Embedded forms render inside your content, for example in a footer or a blog sidebar. Besides the script, they need a placeholder element: 1. **Copy the placeholder.** Open the embed form's page. The **Installation** card shows the placeholder with the form's public token: ```html
``` 2. **Place it.** Paste the placeholder where the form should appear. You can use the same form in several places. 3. **Check the script.** Make sure the [script](#add-the-script) is on the page too. The script fills every `data-emailit-form` placeholder whose token matches a live embed form. It also watches the page, so placeholders added later, for example by a single-page app, are filled when they appear. Embedded forms show every time, whether or not the visitor submitted them before. ## Open a form from your code Call `emailit("show", token)` to open a popup, flyout, full-page form or banner when you choose, for example from a button: ```html ``` - The form opens right away, even if the visitor closed or submitted it before. - The form must be live, and the script must be on the page. - Calls made while the script is still loading are queued and run once it's ready. - For an embed form, `show` re-scans the page for its placeholders. `openForm` works as an alias for `show`. ## Manage a live form ### Update or unpublish Saving changes to a live form, or unpublishing it, reaches your site within about a minute. To take a form off your site, open its page and select **Unpublish**. It goes back to draft and you can publish it again later. ### Reset the public token Each form has a public token, shown as **Public token** on its page. Embed placeholders and `emailit("show", ...)` calls use it. To issue a new one, call [Reset the public token](/docs/api-reference/forms/reset-token/): ```bash curl https://api.emailit.com/v2/forms/frm_8Lq2Wx5nVb3Tk/reset-token \ -X POST \ -H "Authorization: Bearer $EMAILIT_API_KEY" ``` The old token stops working immediately, so update your placeholders and code with the new `token` from the response. Overlay forms that show automatically aren't affected. ## Check submissions Submit the form on your site with your own details, then open the form's page in the dashboard. **Recent submissions** lists each submission with its **Created** time and **Payload**: the submitted values, keyed by each field's property name. Filter the list by creation date. Along with the values, Emailit stores the page URL, the referrer, the visitor's IP address and their browser's user agent with each submission. Mention this in your privacy notice. Submissions aren't added to your contacts or audiences yet. See [Submissions](/docs/forms/#submissions). ## Troubleshooting
The form doesn't appear Check that the form is **Live** and that the script is on the page, with your workspace's key in its URL. If you closed or submitted the form before, it stays hidden in your browser: open the page in a private window, or remove the `emailit_forms_v1` entry from local storage. Remember that only one popup, flyout or full-page form shows per page load.
An embedded form doesn't render Check that the form's type is **Embed**, that it's live, and that the placeholder's `data-emailit-form` value matches the form's current public token. If you reset the token, update the placeholder.
## Related - [Build a form](/docs/forms/build/): Design steps, fields and styles. - [Forms API](/docs/api-reference/forms/): Create, publish and manage forms from code. --- Source: https://emailit.com/docs/forms/install/ --- # Send email with .NET > Send email from ASP.NET Core and .NET with the Emailit NuGet package, MailKit or System.Net.Mail over SMTP, and verify Emailit webhooks. This guide shows how to send email from a .NET app with the official `Emailit` NuGet package, how to use MailKit or `System.Net.Mail` over SMTP instead, and how to verify webhooks in an ASP.NET Core minimal API. ## Prerequisites - .NET 8 or later. - A [verified sending domain](/docs/domains/add-a-domain/), for example `acme.com`. - An [API key](/docs/developers/api-keys/). A Sending Only key restricted to your domain is enough. - Until your workspace has [production access](/docs/workspaces/production-access/), you can only send to the account emails of workspace members. ## Install the SDK ```bash dotnet add package Emailit ``` ## Configure your API key ASP.NET Core reads configuration from environment variables, where `__` separates sections. Set `Emailit__ApiKey` in production: ```bash ``` During development, keep the key in user secrets instead of `appsettings.json`: ```bash dotnet user-secrets init dotnet user-secrets set "Emailit:ApiKey" "secret_••••••••••••••••••••••••••••••••" ``` ## Send an email Register one client and inject it into your endpoints: ```csharp title="Program.cs" using Emailit; using Emailit.Options; using Emailit.Resources; var builder = WebApplication.CreateBuilder(args); builder.Services.AddSingleton(new EmailitClient(builder.Configuration["Emailit:ApiKey"]!)); var app = builder.Build(); app.MapPost("/orders/{id}/confirm", (string id, EmailitClient emailit) => { Email email = emailit.Emails.Send(new EmailSendOptions { From = "Acme ", To = new[] { "ada@example.com" }, Subject = $"Order {id} confirmed", Html = $"

We've received order {id}.

", }); return Results.Ok(new { emailId = email.Id }); }); app.Run(); ``` `Send` is synchronous; for bulk sends, run it from a background service so request threads stay free. The options also accept `Cc`, `Bcc`, `ReplyTo`, `Attachments`, `Template` with `Variables`, `ScheduledAt` and `Tracking`; see [Send an email](/docs/api-reference/emails/send/). If your code already builds `System.Net.Mail.MailMessage` objects, the SDK can send them through the API: ```csharp using System.Net.Mail; var message = new MailMessage("orders@acme.com", "ada@example.com", "Order confirmed", "

Thanks!

") { IsBodyHtml = true, }; Email email = emailit.Emails.Send(message); ``` ### Handle errors ```csharp using Emailit.Exceptions; try { emailit.Emails.Send(options); } catch (RateLimitException) { // 429: back off and retry } catch (AuthenticationException) { // 401: the API key is missing or invalid } catch (UnprocessableEntityException ex) { // 422: for example, the from domain isn't verified logger.LogWarning("Emailit rejected the email: {Message}", ex.Message); } catch (ApiErrorException ex) { logger.LogError("Emailit error {Status}: {Body}", ex.HttpStatus, ex.HttpBody); } ``` ## Send with SMTP instead ### MailKit Microsoft recommends [MailKit](https://github.com/jstedfast/MailKit) for new SMTP code. Install it with `dotnet add package MailKit`: ```csharp using MailKit.Net.Smtp; using MailKit.Security; using MimeKit; var message = new MimeMessage(); message.From.Add(new MailboxAddress("Acme", "orders@acme.com")); message.To.Add(MailboxAddress.Parse("ada@example.com")); message.Subject = "Order confirmed"; message.Body = new TextPart("html") { Text = "

We've received your order.

" }; using var smtp = new SmtpClient(); await smtp.ConnectAsync("smtp.emailit.com", 587, SecureSocketOptions.StartTls); await smtp.AuthenticateAsync("emailit", builder.Configuration["Emailit:ApiKey"]); await smtp.SendAsync(message); await smtp.DisconnectAsync(true); ``` For implicit TLS, connect to port 465 with `SecureSocketOptions.SslOnConnect`. ### System.Net.Mail The built-in client works for simple cases. It only supports STARTTLS, so use port 587 (or 2525 or 2587): ```csharp using System.Net; using System.Net.Mail; using var client = new SmtpClient("smtp.emailit.com", 587) { EnableSsl = true, Credentials = new NetworkCredential("emailit", apiKey), }; client.Send(new MailMessage("orders@acme.com", "ada@example.com", "Order confirmed", "We've received your order.")); ``` See [SMTP settings](/docs/smtp/settings/) for every port. ## Receive webhooks [Create a webhook](/docs/webhooks/set-up/) and store its signing secret as `Emailit:WebhookSecret`. Read the raw body, verify it, then parse the array of events: ```csharp title="Program.cs" using System.Security.Cryptography; using System.Text; using System.Text.Json; app.MapPost("/webhooks/emailit", async (HttpRequest request, IConfiguration config) => { using var reader = new StreamReader(request.Body, Encoding.UTF8); var body = await reader.ReadToEndAsync(); var signature = request.Headers["X-Emailit-Signature"].ToString(); var timestamp = request.Headers["X-Emailit-Timestamp"].ToString(); if (!long.TryParse(timestamp, out var ts) || Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - ts) > 300) { return Results.Unauthorized(); } using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(config["Emailit:WebhookSecret"]!)); var expected = Convert.ToHexString(hmac.ComputeHash(Encoding.UTF8.GetBytes($"{timestamp}.{body}"))).ToLowerInvariant(); if (!CryptographicOperations.FixedTimeEquals(Encoding.UTF8.GetBytes(expected), Encoding.UTF8.GetBytes(signature))) { return Results.Unauthorized(); } // The body is a JSON array of up to 100 events. using var events = JsonDocument.Parse(body); foreach (var evt in events.RootElement.EnumerateArray()) { if (evt.GetProperty("type").GetString() == "email.bounced") { var address = evt.GetProperty("data").GetProperty("object").GetProperty("to").GetString(); // Stop emailing this address. } } return Results.Ok(); }); ``` Return a `2xx` within 30 seconds; other responses are retried. See [Request signature](/docs/webhooks/request-signature/). ## Production tips - **Register the client as a singleton.** One `EmailitClient` per app is enough. - **Queue bulk mail.** Send from a `BackgroundService` or a job library such as Hangfire, and throttle it to stay under your [sending limits](/docs/limits/) (2 emails per second by default). - **Make retries safe.** If you retry after a timeout, send an `Idempotency-Key` header with `HttpClient`; see [Idempotency](/docs/email-api/idempotency/). - **Deduplicate webhooks** by storing each `event_id` you process. ## Next steps - [Send email with the API](/docs/email-api/send-email/): Attachments, scheduling and tracking. - [Webhook event types](/docs/webhooks/event-types/): Every event and its payload. - [SMTP troubleshooting](/docs/smtp/troubleshooting/): Fix authentication and connection errors. - [SDKs and libraries](/docs/sdks/): All official libraries. --- Source: https://emailit.com/docs/frameworks/dotnet/ --- # Send email with Go > Send email from Go with the emailit-go SDK or net/smtp, handle API errors, and verify Emailit webhook signatures in a net/http handler. This guide shows how to send email from a Go service with the official `emailit-go` SDK, how to use `net/smtp` instead, and how to verify webhooks in a `net/http` handler. ## Prerequisites - Go 1.21 or later. - A [verified sending domain](/docs/domains/add-a-domain/), for example `acme.com`. - An [API key](/docs/developers/api-keys/). A Sending Only key restricted to your domain is enough. - Until your workspace has [production access](/docs/workspaces/production-access/), you can only send to the account emails of workspace members. ## Install the SDK ```bash go get github.com/emailit/emailit-go/v2 ``` The SDK only depends on the Go standard library. ## Configure your API key Pass the key to your process as an environment variable: ```bash ``` ## Send an email ```go title="main.go" package main "log" "os" "github.com/emailit/emailit-go/v2" ) func main() { client := emailit.NewClient(os.Getenv("EMAILIT_API_KEY")) email, err := client.Emails.Send(&emailit.SendEmailRequest{ From: "Acme ", To: []string{"ada@example.com"}, Subject: "Your login code", Text: "Your code is 482913. It expires in 10 minutes.", Html: "

Your code is 482913. It expires in 10 minutes.

", }) if err != nil { log.Fatal(err) } log.Println("sent", email.Id) // em_… } ``` Create the client once and share it; it's safe to reuse across requests. To send a stored template, set `Template` to its alias or `tem_` ID and pass `Variables`. To change timeouts, pass your own client: `emailit.NewClient(key, emailit.WithHTTPClient(&http.Client{Timeout: 10 * time.Second}))`. ### Handle errors The SDK returns ordinary Go errors with helpers to classify them: ```go email, err := client.Emails.Send(req) if err != nil { switch { case emailit.IsRateLimitError(err): // 429: back off and retry case emailit.IsAuthenticationError(err): // 401: the API key is missing or invalid case emailit.IsUnprocessableEntityError(err): // 422: for example, the from domain isn't verified } var apiErr *emailit.APIError if errors.As(err, &apiErr) { log.Printf("emailit: %d %s", apiErr.StatusCode, apiErr.Message) } return err } ``` ## Send with SMTP instead The standard library's `net/smtp` can send through the Emailit relay. `smtp.SendMail` upgrades the connection with STARTTLS before it authenticates: ```go title="smtp.go" package main "log" "net/smtp" "os" "strings" ) func main() { auth := smtp.PlainAuth("", "emailit", os.Getenv("EMAILIT_API_KEY"), "smtp.emailit.com") msg := strings.Join([]string{ "From: Acme ", "To: ada@example.com", "Subject: Your login code", "MIME-Version: 1.0", "Content-Type: text/plain; charset=UTF-8", "", "Your code is 482913. It expires in 10 minutes.", }, "\r\n") err := smtp.SendMail("smtp.emailit.com:587", auth, "hello@acme.com", []string{"ada@example.com"}, []byte(msg)) if err != nil { log.Fatal(err) } } ``` `net/smtp` is minimal: you build the message yourself, and it has no implicit TLS on port 465. For HTML, attachments or connection reuse, use a maintained mail library or the API. If port 587 is blocked, use `smtp.emailit.com:2525`. See [SMTP settings](/docs/smtp/settings/). ## Receive webhooks [Create a webhook](/docs/webhooks/set-up/) and store its signing secret in `EMAILIT_WEBHOOK_SECRET`. The handler reads the raw body, checks the signature, then decodes the array of events: ```go title="webhooks.go" package main "crypto/hmac" "crypto/sha256" "encoding/hex" "encoding/json" "io" "net/http" "os" "strconv" "time" ) type emailitEvent struct { EventID string `json:"event_id"` Type string `json:"type"` Data struct { Object map[string]any `json:"object"` } `json:"data"` } func validSignature(body []byte, signature, timestamp, secret string) bool { ts, err := strconv.ParseInt(timestamp, 10, 64) if err != nil || signature == "" { return false } if age := time.Now().Unix() - ts; age > 300 || age < -300 { return false } mac := hmac.New(sha256.New, []byte(secret)) mac.Write([]byte(timestamp + ".")) mac.Write(body) expected := hex.EncodeToString(mac.Sum(nil)) return hmac.Equal([]byte(expected), []byte(signature)) } func emailitWebhook(w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(http.MaxBytesReader(w, r.Body, 10<<20)) if err != nil { http.Error(w, "bad request", http.StatusBadRequest) return } if !validSignature(body, r.Header.Get("X-Emailit-Signature"), r.Header.Get("X-Emailit-Timestamp"), os.Getenv("EMAILIT_WEBHOOK_SECRET")) { http.Error(w, "invalid signature", http.StatusUnauthorized) return } var events []emailitEvent // up to 100 events per request if err := json.Unmarshal(body, &events); err != nil { http.Error(w, "bad request", http.StatusBadRequest) return } for _, event := range events { if event.Type == "email.bounced" { // Stop emailing event.Data.Object["to"]. } } w.WriteHeader(http.StatusOK) } func main() { http.HandleFunc("POST /webhooks/emailit", emailitWebhook) http.ListenAndServe(":8080", nil) } ``` The `POST /path` pattern needs Go 1.22 or later; on Go 1.21, register `/webhooks/emailit` and check `r.Method` yourself. Return a `2xx` within 30 seconds; other responses are retried. See [Request signature](/docs/webhooks/request-signature/). ## Production tips - **Stay under your rate limit.** New workspaces can send 2 emails per second and 5,000 per day by default. Send bulk mail from a worker with a rate limiter such as `golang.org/x/time/rate`. See [Limits](/docs/limits/). - **Make retries safe.** If you retry after a timeout, send an `Idempotency-Key` header with `net/http` so the email isn't sent twice. See [Idempotency](/docs/email-api/idempotency/). - **Deduplicate webhooks** by storing each `EventID` you process. - **Set timeouts.** Use a context or an HTTP client timeout so a slow network doesn't hold request goroutines. ## Next steps - [Send email with the API](/docs/email-api/send-email/): Attachments, scheduling and tracking. - [Rate limits](/docs/api-reference/rate-limits/): Headers and how to back off. - [Webhook event types](/docs/webhooks/event-types/): Every event and its payload. - [SDKs and libraries](/docs/sdks/): All official libraries. --- Source: https://emailit.com/docs/frameworks/go/ --- # Send email with Java > Send email from Java with the emailit-java SDK or Spring Boot and Jakarta Mail over SMTP, and verify Emailit webhooks in a Spring controller. This guide shows how to send email from Java with the official `emailit-java` SDK, how to use Spring Boot's mail starter or Jakarta Mail over SMTP instead, and how to verify webhooks in a Spring controller. ## Prerequisites - Java 11 or later. The Spring examples use Spring Boot 3, which needs Java 17. - A [verified sending domain](/docs/domains/add-a-domain/), for example `acme.com`. - An [API key](/docs/developers/api-keys/). A Sending Only key restricted to your domain is enough. - Until your workspace has [production access](/docs/workspaces/production-access/), you can only send to the account emails of workspace members. ## Install the SDK Add the dependency, using the latest version from the [emailit-java repository](https://github.com/emailit/emailit-java): ```xml title="pom.xml" com.emailit emailit-java VERSION ``` ```groovy title="build.gradle" implementation 'com.emailit:emailit-java:VERSION' ``` ## Configure your API key Provide the key as an environment variable: ```bash ``` In Spring Boot, map it to a property so you can inject it: ```properties title="application.properties" emailit.api-key=${EMAILIT_API_KEY} ``` ## Send an email ```java title="SendEmail.java" public class SendEmail { public static void main(String[] args) throws EmailitException { EmailitClient emailit = new EmailitClient(System.getenv("EMAILIT_API_KEY")); EmailSendParams params = EmailSendParams.builder() .setFrom("Acme ") .setTo(List.of("ada@example.com")) .setSubject("Your order has shipped") .setHtml("

Order #1042 is on its way.

") .build(); EmailitObject email = emailit.emails().send(params); System.out.println(email.getString("id")); // em_… } } ``` In Spring Boot, register the client once as a bean and inject it where you send: ```java title="EmailitConfig.java" @Configuration public class EmailitConfig { @Bean EmailitClient emailitClient(@Value("${emailit.api-key}") String apiKey) { return new EmailitClient(apiKey); } } ``` ### Handle errors The SDK throws typed exceptions that extend `EmailitException`: ```java try { emailit.emails().send(params); } catch (RateLimitException e) { // 429: back off and retry } catch (AuthenticationException e) { // 401: the API key is missing or invalid } catch (UnprocessableEntityException e) { // 422: for example, the from domain isn't verified } catch (EmailitException e) { log.error("Emailit error {}: {}", e.getHttpStatus(), e.getHttpBody()); } ``` ## Send with SMTP instead ### Spring Boot Add `spring-boot-starter-mail` and configure the relay: ```properties title="application.properties" spring.mail.host=smtp.emailit.com spring.mail.port=587 spring.mail.username=emailit spring.mail.password=${EMAILIT_API_KEY} spring.mail.properties.mail.smtp.auth=true spring.mail.properties.mail.smtp.starttls.enable=true spring.mail.properties.mail.smtp.starttls.required=true ``` Then send with `JavaMailSender`: ```java title="WelcomeMailer.java" @Service public class WelcomeMailer { private final JavaMailSender mailSender; public WelcomeMailer(JavaMailSender mailSender) { this.mailSender = mailSender; } public void sendWelcome(String to) { SimpleMailMessage message = new SimpleMailMessage(); message.setFrom("Acme "); message.setTo(to); message.setSubject("Welcome to Acme"); message.setText("Thanks for signing up."); mailSender.send(message); } } ``` Use `MimeMessageHelper` for HTML and attachments. ### Jakarta Mail without Spring Plain Jakarta Mail (formerly JavaMail) uses the same `mail.smtp.*` properties: ```java Properties props = new Properties(); props.put("mail.smtp.host", "smtp.emailit.com"); props.put("mail.smtp.port", "587"); props.put("mail.smtp.auth", "true"); props.put("mail.smtp.starttls.enable", "true"); props.put("mail.smtp.starttls.required", "true"); Session session = Session.getInstance(props, new Authenticator() { @Override protected PasswordAuthentication getPasswordAuthentication() { return new PasswordAuthentication("emailit", System.getenv("EMAILIT_API_KEY")); } }); ``` If port 587 is blocked on your network, use 2525 or 2587 with the same settings. See [SMTP settings](/docs/smtp/settings/). ## Receive webhooks [Create a webhook](/docs/webhooks/set-up/) and store its signing secret in `EMAILIT_WEBHOOK_SECRET`. Accept the body as a `String` so you verify exactly what Emailit signed: ```java title="EmailitWebhookController.java" @RestController public class EmailitWebhookController { private final ObjectMapper mapper = new ObjectMapper(); private final String secret = System.getenv("EMAILIT_WEBHOOK_SECRET"); @PostMapping("/webhooks/emailit") public ResponseEntity receive( @RequestBody String body, @RequestHeader(value = "X-Emailit-Signature", defaultValue = "") String signature, @RequestHeader(value = "X-Emailit-Timestamp", defaultValue = "0") long timestamp) throws Exception { if (Math.abs(System.currentTimeMillis() / 1000 - timestamp) > 300 || !isValid(body, signature, timestamp)) { return ResponseEntity.status(401).build(); } // The body is a JSON array of up to 100 events. for (JsonNode event : mapper.readTree(body)) { if ("email.bounced".equals(event.path("type").asText())) { String address = event.path("data").path("object").path("to").asText(); // Stop emailing this address. } } return ResponseEntity.ok().build(); } private boolean isValid(String body, String signature, long timestamp) throws Exception { Mac mac = Mac.getInstance("HmacSHA256"); mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256")); byte[] digest = mac.doFinal((timestamp + "." + body).getBytes(StandardCharsets.UTF_8)); String expected = HexFormat.of().formatHex(digest); return MessageDigest.isEqual( expected.getBytes(StandardCharsets.UTF_8), signature.getBytes(StandardCharsets.UTF_8)); } } ``` If Spring Security is enabled, permit this path and exclude it from CSRF protection. Return a `2xx` within 30 seconds; other responses are retried. See [Request signature](/docs/webhooks/request-signature/). ## Production tips - **Reuse clients.** Create one `EmailitClient` (or rely on Spring's single `JavaMailSender`) instead of one per message. - **Send asynchronously.** Use `@Async`, a message queue or a scheduled job for bulk mail, and limit throughput to stay under your [sending limits](/docs/limits/) (2 emails per second by default). - **Make retries safe.** If you retry after a timeout, send an `Idempotency-Key` header with `java.net.http.HttpClient`; see [Idempotency](/docs/email-api/idempotency/). - **Deduplicate webhooks** by storing each `event_id` you process. ## Next steps - [Send email with the API](/docs/email-api/send-email/): Attachments, scheduling and tracking. - [Webhook event types](/docs/webhooks/event-types/): Every event and its payload. - [SMTP troubleshooting](/docs/smtp/troubleshooting/): Fix authentication and connection errors. - [SDKs and libraries](/docs/sdks/): All official libraries. --- Source: https://emailit.com/docs/frameworks/java/ --- # Send email with Laravel > Send Laravel mail through Emailit with the emailit/emailit-laravel mail transport and facade, or plain SMTP, and verify Emailit webhooks. This guide shows two ways to send Laravel mail through Emailit: the `emailit/emailit-laravel` package, which adds an `emailit` mail transport and an `Emailit` facade, and plain SMTP with Laravel's built-in mailer. It ends with a webhook route that verifies signatures. ## Prerequisites - PHP 8.1 or later and Laravel 10, 11 or 12. - A [verified sending domain](/docs/domains/add-a-domain/), for example `acme.com`. - An [API key](/docs/developers/api-keys/). A Sending Only key restricted to your domain is enough. - Until your workspace has [production access](/docs/workspaces/production-access/), you can only send to the account emails of workspace members. ## Option 1: the Emailit Laravel package The package sends your existing Mailables, Markdown mailables, notifications and queued mail through the Emailit API, with no changes to your mail code. ### Install the package ```bash composer require emailit/emailit-laravel ``` The service provider is auto-discovered. ### Configure the transport Add your key and make Emailit the default mailer in `.env`: ```bash title=".env" EMAILIT_API_KEY=secret_•••••••••••••••••••••••••••••••• MAIL_MAILER=emailit MAIL_FROM_ADDRESS=hello@acme.com MAIL_FROM_NAME="Acme" ``` Register the mailer in `config/mail.php`: ```php title="config/mail.php" 'mailers' => [ // ... 'emailit' => [ 'transport' => 'emailit', ], ], ``` `MAIL_FROM_ADDRESS` must be on a verified sending domain. To change the API base URL, publish the config file with `php artisan vendor:publish --tag=emailit-config`; you don't need to for normal use. ### Send a Mailable Create a Mailable as usual: ```bash php artisan make:mail WelcomeEmail ``` ```php title="app/Mail/WelcomeEmail.php" namespace App\Mail; use App\Models\User; use Illuminate\Bus\Queueable; use Illuminate\Mail\Mailable; use Illuminate\Mail\Mailables\Content; use Illuminate\Mail\Mailables\Envelope; use Illuminate\Queue\SerializesModels; class WelcomeEmail extends Mailable { use Queueable, SerializesModels; public function __construct(public User $user) {} public function envelope(): Envelope { return new Envelope(subject: 'Welcome to Acme'); } public function content(): Content { return new Content(view: 'emails.welcome'); } } ``` Send it, or queue it so the request doesn't wait for the API: ```php use App\Mail\WelcomeEmail; use Illuminate\Support\Facades\Mail; Mail::to($user)->send(new WelcomeEmail($user)); Mail::to($user)->queue(new WelcomeEmail($user)); ``` ### Use the facade for API features The `Emailit` facade exposes the full [PHP SDK](/docs/frameworks/php/), for features Laravel's mailer doesn't model, such as stored templates and scheduled sends: ```php use Emailit\Laravel\Facades\Emailit; $email = Emailit::emails()->send([ 'from' => 'Acme ', 'to' => $user->email, 'template' => 'welcome', 'variables' => ['first_name' => $user->first_name], 'scheduled_at' => 'tomorrow at 9am', ]); $email->id; // em_… ``` The facade also covers domains, contacts, audiences, suppressions, webhooks and the other resources. If you prefer dependency injection, type-hint `Emailit\EmailitClient` in a controller or job and Laravel resolves it with your configured key. Catch typed exceptions to handle failures: ```php use Emailit\Exceptions\ApiErrorException; use Emailit\Exceptions\RateLimitException; try { Emailit::emails()->send($payload); } catch (RateLimitException $e) { // 429: release the job back to the queue and try again later } catch (ApiErrorException $e) { report($e); // $e->getHttpStatus() has the status code } ``` ## Option 2: plain SMTP If you'd rather not add a package, point Laravel's SMTP mailer at the Emailit relay: ```bash title=".env" MAIL_MAILER=smtp MAIL_HOST=smtp.emailit.com MAIL_PORT=587 MAIL_USERNAME=emailit MAIL_PASSWORD=secret_•••••••••••••••••••••••••••••••• MAIL_ENCRYPTION=tls MAIL_FROM_ADDRESS=hello@acme.com MAIL_FROM_NAME="Acme" ``` On port 587 Laravel's mailer upgrades the connection with STARTTLS. Older `config/mail.php` files read `MAIL_ENCRYPTION` and newer ones ignore it, so it's safe to keep. For implicit TLS, use port `465`; if your host blocks 587, use `2525` or `2587`. Mailables, notifications and queues work exactly as with the package. See [SMTP settings](/docs/smtp/settings/). Over SMTP you can't use stored templates or `scheduled_at`; use the facade or the API for those. ## Receive webhooks [Create a webhook](/docs/webhooks/set-up/) that points to `https://your-app.com/webhooks/emailit`, then store its signing secret: ```bash title=".env" EMAILIT_WEBHOOK_SECRET=whsec_•••••••• ``` ```php title="config/services.php" 'emailit' => [ 'webhook_secret' => env('EMAILIT_WEBHOOK_SECRET'), ], ``` Add a controller that checks the signature against the raw body: ```php title="app/Http/Controllers/EmailitWebhookController.php" namespace App\Http\Controllers; use Illuminate\Http\Request; class EmailitWebhookController extends Controller { public function __invoke(Request $request) { $payload = $request->getContent(); $signature = (string) $request->header('X-Emailit-Signature'); $timestamp = (string) $request->header('X-Emailit-Timestamp'); $expected = hash_hmac('sha256', $timestamp.'.'.$payload, config('services.emailit.webhook_secret')); if (abs(time() - (int) $timestamp) > 300 || ! hash_equals($expected, $signature)) { abort(401, 'Invalid signature'); } // The body is a JSON array of up to 100 events. foreach (json_decode($payload, true) as $event) { match ($event['type']) { 'email.bounced', 'email.complained' => $this->stopEmailing($event['data']['object']['to']), default => null, }; } return response()->noContent(); } private function stopEmailing(string $address): void { // Mark the address as undeliverable in your database. } } ``` Register the route and exclude it from CSRF protection, because Emailit can't send a CSRF token: ```php title="routes/web.php" use App\Http\Controllers\EmailitWebhookController; Route::post('/webhooks/emailit', EmailitWebhookController::class); ``` ```php title="bootstrap/app.php (Laravel 11 and 12)" ->withMiddleware(function (Middleware $middleware) { $middleware->validateCsrfTokens(except: ['webhooks/emailit']); }) ``` On Laravel 10, add `'webhooks/emailit'` to the `$except` array in `app/Http/Middleware/VerifyCsrfToken.php` instead. Return a `2xx` within 30 seconds, and push slow work onto a queue. See [Request signature](/docs/webhooks/request-signature/). ## Production tips - **Queue your mail.** Use `queue()` or `ShouldQueue` so web requests don't wait on email, and throttle bulk jobs (for example with `Redis::throttle`) to stay under your [sending limits](/docs/limits/). New workspaces can send 2 emails per second by default. - **Cache config safely.** After `php artisan config:cache`, `env()` only works inside config files. Read the key through config, as the package does. - **Use a dedicated key.** Give each app and environment its own Sending Only key so you can rotate one without touching the others. See [API keys](/docs/developers/api-keys/). - **Deduplicate webhooks.** Store each `event_id` you process and skip repeats, because failed deliveries are retried. ## Next steps - [PHP SDK guide](/docs/frameworks/php/): The client behind the facade. - [Templates](/docs/templates/): Design emails in Emailit and send them by alias. - [Webhook event types](/docs/webhooks/event-types/): Every event and its payload. - [SMTP troubleshooting](/docs/smtp/troubleshooting/): Fix authentication and connection errors. --- Source: https://emailit.com/docs/frameworks/laravel/ --- # Send email with Next.js > Send email from Next.js route handlers and server actions with the Emailit Node.js SDK, keep the API key server-side and verify webhooks. This guide shows how to send email from a Next.js app with the `@emailit/node` SDK, from both a route handler and a server action, and how to verify Emailit webhooks. The examples use the App Router; the Pages Router is covered at the end of the sending section. ## Prerequisites - Next.js 14 or later. The SDK needs Node.js 18 or later. - A [verified sending domain](/docs/domains/add-a-domain/), for example `acme.com`. - An [API key](/docs/developers/api-keys/). A Sending Only key restricted to your domain is enough. - Until your workspace has [production access](/docs/workspaces/production-access/), you can only send to the account emails of workspace members. ## Install the SDK ```bash npm install @emailit/node server-only ``` `server-only` makes the build fail if a client component imports the module that holds your key. ## Configure your API key Add the key to `.env.local` for development and to your host's environment variables (for example, Vercel project settings) for production: ```bash title=".env.local" EMAILIT_API_KEY=secret_•••••••••••••••••••••••••••••••• EMAILIT_WEBHOOK_SECRET=whsec_•••••••• ``` > **Never expose the key to the browser:** Don't prefix these variables with `NEXT_PUBLIC_`, and don't call Emailit from client components. Anything prefixed `NEXT_PUBLIC_` is bundled into JavaScript that every visitor can read. Create one client in a server-only module: ```typescript title="lib/emailit.ts" ``` The SDK has no TypeScript declarations yet. If the compiler complains, add a declaration file: ```typescript title="types/emailit.d.ts" declare module '@emailit/node'; ``` ## Send from a route handler A route handler is the right place for sends triggered by your own frontend or by other services. This contact form handler sends to a fixed internal address, so visitors can't use it to email arbitrary people: ```typescript title="app/api/contact/route.ts" const { email, message } = await request.json(); if (typeof email !== 'string' || typeof message !== 'string' || !email.includes('@')) { return Response.json({ error: 'Invalid input' }, { status: 400 }); } try { const sent = await emailit.emails.send({ from: 'Acme website ', to: 'support@acme.com', reply_to: email, subject: 'New contact form message', text: message, }); return Response.json({ id: sent.id }); } catch (error) { console.error(error); return Response.json({ error: 'Could not send the message' }, { status: 502 }); } } ``` Call it from the browser with `fetch('/api/contact', { method: 'POST', body: JSON.stringify(data) })`. The key never leaves the server. ## Send from a server action Server actions run on the server, so they can use the client directly: ```typescript title="app/signup/actions.ts" 'use server'; const email = formData.get('email'); if (typeof email !== 'string' || !email.includes('@')) return; // Create the account here, then send the welcome email. await emailit.emails.send({ from: 'Acme ', to: email, template: 'welcome', variables: { email }, }); } ``` ```tsx title="app/signup/page.tsx" return (
); } ``` `template` takes a template alias or `tem_` ID; see [Templates](/docs/templates/). Server actions are public endpoints, so validate input and add your usual bot and abuse protection before sending. ### Pages Router With the Pages Router, send from an API route instead: ```typescript title="pages/api/contact.ts" if (req.method !== 'POST') return res.status(405).end(); const sent = await emailit.emails.send({ from: 'Acme website ', to: 'support@acme.com', subject: 'New contact form message', text: String(req.body.message ?? ''), }); res.status(200).json({ id: sent.id }); } ``` ## Send with SMTP instead If you already use Nodemailer, configure it with the Emailit relay inside a route handler or server action (SMTP needs the Node.js runtime, not the Edge runtime): ```typescript title="lib/mailer.ts" host: 'smtp.emailit.com', port: 587, secure: false, requireTLS: true, auth: { user: 'emailit', pass: process.env.EMAILIT_API_KEY }, }); ``` Then call `await mailer.sendMail({ from, to, subject, html })`. Serverless functions open a new SMTP connection on most invocations, so the API is usually faster on Vercel and similar platforms. See [SMTP settings](/docs/smtp/settings/). ## Receive webhooks [Create a webhook](/docs/webhooks/set-up/) that points to `https://your-app.com/api/webhooks/emailit`. The handler must verify the signature against the raw body, so read it with `request.text()` before parsing: ```typescript title="app/api/webhooks/emailit/route.ts" type EmailitEvent = { event_id: string; type: string; data: { object: Record }; }; function isValid(rawBody: string, signature: string | null, timestamp: string | null) { if (!signature || !timestamp) return false; const age = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp)); if (!Number.isFinite(age) || age > 300) return false; const expected = createHmac('sha256', process.env.EMAILIT_WEBHOOK_SECRET!) .update(`${timestamp}.${rawBody}`) .digest('hex'); const a = Buffer.from(expected); const b = Buffer.from(signature); return a.length === b.length && timingSafeEqual(a, b); } const rawBody = await request.text(); const valid = isValid( rawBody, request.headers.get('x-emailit-signature'), request.headers.get('x-emailit-timestamp'), ); if (!valid) return new Response('Invalid signature', { status: 401 }); // Each request carries an array of up to 100 events. const events = JSON.parse(rawBody) as EmailitEvent[]; for (const event of events) { if (event.type === 'email.bounced') { // Flag event.data.object.to in your database. } } return new Response(null, { status: 200 }); } ``` Return a `2xx` within 30 seconds; other responses are retried. See [Request signature](/docs/webhooks/request-signature/) and [Event types](/docs/webhooks/event-types/). ## Production tips - **Keep sends on the server.** Only `lib/emailit.ts`, route handlers and server actions should touch the key. `server-only` enforces this at build time. - **Never let the client choose the sender.** Hard-code `from`, and only accept `to` from the browser when it's the signed-in user's own address. - **Protect public endpoints.** Rate-limit contact forms and sign-up actions, and add a CAPTCHA if bots find them. Every email costs credits and counts against your [sending limits](/docs/limits/). - **Set variables per environment.** Use separate keys for preview and production deployments so you can revoke one without affecting the other. ## Next steps - [Node.js guide](/docs/frameworks/nodejs/): Error handling and Nodemailer in more detail. - [Send email with the API](/docs/email-api/send-email/): Attachments, scheduling and tracking. - [Templates](/docs/templates/): Design emails once, send them by alias. - [API keys](/docs/developers/api-keys/): Scopes, domain restrictions and rotation. --- Source: https://emailit.com/docs/frameworks/nextjs/ --- # Send email with Node.js > Send email from Node.js with the @emailit/node SDK or Nodemailer over SMTP, and verify Emailit webhooks in an Express route. This guide shows how to send email from a Node.js app with the official `@emailit/node` SDK, how to use Nodemailer over SMTP instead, and how to receive signed webhooks in Express. ## Prerequisites - Node.js 18 or later. - A [verified sending domain](/docs/domains/add-a-domain/), for example `acme.com`. - An [API key](/docs/developers/api-keys/). A Sending Only key is enough to send. - Until your workspace has [production access](/docs/workspaces/production-access/), you can only send to the account emails of workspace members. Use your own address while you test. ## Install the SDK ```bash npm install @emailit/node ``` The package is ESM-only. Use `import` syntax, or `await import('@emailit/node')` from CommonJS code. ## Configure your API key Keep the key out of your code. Put it in an environment variable: ```bash title=".env" EMAILIT_API_KEY=secret_•••••••••••••••••••••••••••••••• ``` Load the file with `node --env-file=.env` (Node.js 20.6 and later), a package such as `dotenv`, or your platform's secret settings. Add `.env` to `.gitignore`. ## Send an email Create one client and reuse it across requests: ```javascript title="send.mjs" const emailit = new Emailit(process.env.EMAILIT_API_KEY); const email = await emailit.emails.send({ from: 'Acme ', to: 'ada@example.com', subject: 'Welcome to Acme', html: '

Thanks for signing up.

', text: 'Thanks for signing up.', }); console.log(email.id); // em_… ``` Run it with `node --env-file=.env send.mjs`. The email appears in **Email API → Emails** within seconds. In an Express app, send from the route that triggers the email: ```javascript title="server.mjs" const app = express(); const emailit = new Emailit(process.env.EMAILIT_API_KEY); app.post('/signup', express.json(), async (req, res) => { const { email } = req.body; // Create the user here, then send the welcome email. const sent = await emailit.emails.send({ from: 'Acme ', to: email, template: 'welcome', variables: { email }, }); res.status(201).json({ emailId: sent.id }); }); app.listen(3000); ``` `template` takes a template alias or `tem_` ID, and `variables` fills it in. See [Templates](/docs/templates/) and the full list of fields in [Send an email](/docs/api-reference/emails/send/). ## Handle errors The SDK throws typed errors, so you can react to each failure: ```javascript AuthenticationException, RateLimitException, UnprocessableEntityException, ApiErrorException, } from '@emailit/node'; try { await emailit.emails.send(message); } catch (err) { if (err instanceof RateLimitException) { // 429: wait err.jsonBody.retry_after seconds, then retry } else if (err instanceof AuthenticationException) { // 401: the API key is missing or invalid } else if (err instanceof UnprocessableEntityException) { // 422: for example, the from domain isn't verified } else if (err instanceof ApiErrorException) { console.error(err.httpStatus, err.jsonBody); } else { throw err; } } ``` ## Send with SMTP instead If your app already uses [Nodemailer](https://nodemailer.com), point it at the Emailit SMTP relay. Install it with `npm install nodemailer`: ```javascript title="mailer.mjs" const transporter = nodemailer.createTransport({ host: 'smtp.emailit.com', port: 587, secure: false, // upgraded with STARTTLS after connecting requireTLS: true, auth: { user: 'emailit', pass: process.env.EMAILIT_API_KEY, }, }); const info = await transporter.sendMail({ from: 'Acme ', to: 'ada@example.com', subject: 'Welcome to Acme', text: 'Thanks for signing up.', html: '

Thanks for signing up.

', }); console.log(info.response); // 250 2.0.0 OK: queued as em_… ``` For port 465, set `port: 465` and `secure: true`. If your network blocks 587, use 2525 or 2587 with the same settings. See [SMTP settings](/docs/smtp/settings/). ## Receive webhooks [Create a webhook](/docs/webhooks/set-up/) that points to `https://your-app.com/webhooks/emailit` and copy its signing secret (`whsec_…`) into `EMAILIT_WEBHOOK_SECRET`. Emailit signs each request with an HMAC-SHA256 of `timestamp.rawBody`, so the route must read the raw body. Use `express.raw()` on this route, not `express.json()`: ```javascript title="webhooks.mjs" function verifyEmailitSignature(rawBody, signature, timestamp, secret) { if (!signature || !timestamp) return false; const age = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp)); if (!Number.isFinite(age) || age > 300) return false; // reject replays older than 5 minutes const expected = createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex'); const a = Buffer.from(expected); const b = Buffer.from(signature); return a.length === b.length && timingSafeEqual(a, b); } webhooks.post('/webhooks/emailit', express.raw({ type: 'application/json' }), (req, res) => { const rawBody = req.body.toString('utf8'); const valid = verifyEmailitSignature( rawBody, req.get('x-emailit-signature'), req.get('x-emailit-timestamp'), process.env.EMAILIT_WEBHOOK_SECRET, ); if (!valid) return res.status(401).send('Invalid signature'); // The body is an array of up to 100 events. const events = JSON.parse(rawBody); for (const event of events) { switch (event.type) { case 'email.delivered': // event.data.object.id is the em_ ID break; case 'email.bounced': case 'email.complained': // stop emailing event.data.object.to break; } } res.sendStatus(200); }); ``` Mount the router with `app.use(webhooks)` before any global `express.json()` middleware. Return a `2xx` within 30 seconds; anything else is retried. Read [Request signature](/docs/webhooks/request-signature/) and [Retries and failures](/docs/webhooks/retries-and-failures/) for details. ## Production tips - **Scope the key.** Use a Sending Only key restricted to your sending domain for the app server. Keep Full Access keys for admin scripts. - **Stay under your rate limit.** New workspaces can send 2 emails per second and 5,000 per day by default, shared between the API and SMTP. Send bulk jobs from a queue. With Nodemailer, `pool: true, rateLimit: 2` keeps a pooled transport at 2 messages per second. See [Limits](/docs/limits/). - **Make retries safe.** If you retry a send after a timeout, send an `Idempotency-Key` header so Emailit doesn't send twice. The SDK doesn't set custom headers, so use `fetch` for those calls; see [Idempotency](/docs/email-api/idempotency/). - **Process webhooks idempotently.** Store each `event_id` you've handled and skip duplicates, and do slow work in a background job after you return `200`. - **Use TypeScript?** The SDK has no type declarations yet. Add `declare module '@emailit/node';` to a `.d.ts` file in your project. ## Next steps - [Send email with the API](/docs/email-api/send-email/): Attachments, scheduling, tracking and metadata. - [Webhook event types](/docs/webhooks/event-types/): Every event and its payload. - [Next.js](/docs/frameworks/nextjs/): Route handlers and server actions. - [SDKs and libraries](/docs/sdks/): All official libraries. --- Source: https://emailit.com/docs/frameworks/nodejs/ --- # Send email with Nuxt > Send email from a Nuxt server API route with the Emailit Node.js SDK, keep the key in private runtimeConfig and verify webhooks with h3. This guide shows how to send email from a Nuxt 3 app. You call Emailit from a server API route, keep the API key in private `runtimeConfig`, and verify webhooks with h3 helpers. ## Prerequisites - Nuxt 3 with a Node.js 18+ server preset. - A [verified sending domain](/docs/domains/add-a-domain/), for example `acme.com`. - An [API key](/docs/developers/api-keys/). A Sending Only key restricted to your domain is enough. - Until your workspace has [production access](/docs/workspaces/production-access/), you can only send to the account emails of workspace members. ## Install the SDK ```bash npm install @emailit/node ``` ## Configure your API key Declare private runtime config keys. Keys outside `public` are only available on the server: ```typescript title="nuxt.config.ts" runtimeConfig: { emailitApiKey: '', emailitWebhookSecret: '', }, }); ``` Nuxt fills them from environment variables with the `NUXT_` prefix: ```bash title=".env" NUXT_EMAILIT_API_KEY=secret_•••••••••••••••••••••••••••••••• NUXT_EMAILIT_WEBHOOK_SECRET=whsec_•••••••• ``` > **Keep the key out of runtimeConfig.public:** Values under `runtimeConfig.public` are sent to the browser. Never put the API key there, and never call Emailit from a component or page. Add a small helper. Files in `server/utils` are auto-imported in server routes: ```typescript title="server/utils/emailit.ts" return new Emailit(useRuntimeConfig(event).emailitApiKey); } ``` If TypeScript reports a missing declaration for `@emailit/node`, add `declare module '@emailit/node';` to a `.d.ts` file in your project. ## Send an email Create a server API route. This contact form route sends to a fixed internal address, so visitors can't use it to email arbitrary people: ```typescript title="server/api/contact.post.ts" const { email, message } = await readBody(event); if (typeof email !== 'string' || typeof message !== 'string' || !email.includes('@')) { throw createError({ statusCode: 400, statusMessage: 'Invalid input' }); } const sent = await emailitClient(event).emails.send({ from: 'Acme website ', to: 'support@acme.com', reply_to: email, subject: 'New contact form message', text: message, }); return { id: sent.id }; }); ``` Call it from a page or component with `$fetch`: ```vue title="pages/contact.vue"