> For clean Markdown of any page, append .md to the page URL. > For a complete documentation index, see https://docs.ibee.co.in/docs/tools/email-service/llms.txt. > For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.ibee.co.in/_mcp/server. # Email Service > Send transactional email from your own subdomain over an HTTPS API. Delegate a subdomain to IBEE, register sender addresses, send with a send-only Email Token, and track delivery with activity, suppressions, and signed webhooks. > **Note** > > Email Service is enabled per organization. If **Email Service** is not in your sidebar, contact [support](/docs/support/contact-support) to request access. Email Service sends transactional email, such as order confirmations, password resets, and notifications, from a subdomain you own, like `email.acme.com`. Your application sends each message with one HTTPS request, and the portal shows its delivery status. * **Your domain, managed authentication.** You delegate one unused subdomain to IBEE with two NS records. IBEE then creates and maintains the SPF, DKIM, DMARC, and return-path (bounce) records inside that subdomain. * **HTTPS API only.** Messages are sent with `POST /v1/emails`. SMTP relay is not available. * **Send-only.** Sender addresses are identities for outgoing mail, not mailboxes. Email Service does not receive email. Domains, senders, suppressions, and webhooks are managed in the portal. They have no public API yet. ## Before you begin * An organization and a workspace. Email Service resources belong to a workspace. * Access to the DNS settings of your company domain, such as `acme.com`, at your DNS provider. * An unused subdomain to send from, such as `email`, `notify`, or `updates`. It must not already serve a website or a mailbox. * To create Email Tokens: an organization admin whose account is verified ([Verify your identity](/docs/getting-started/account-setup/verify-your-identity)). ## How it works ### Add a sending domain Choose your company domain and a sending subdomain. IBEE prepares a DNS zone for the subdomain and gives you its nameservers. ### Delegate the subdomain Add the NS records at the DNS provider for your company domain. Only the sending subdomain is delegated to IBEE. Your company domain's nameservers, website, and existing mailboxes stay unchanged. ### Verify IBEE checks the delegation and then configures email authentication (SPF, DKIM, DMARC, and return-path records) inside the delegated subdomain. ### Register senders Add the exact From addresses your application will use, such as `orders@email.acme.com`. ### Create an Email Token and send Create a send-only Email Token for the domain, then call `POST /v1/emails` from your application server. Delivery results appear in **Activity** and can be sent to your webhook. To open Email Service, select your organization and workspace, then click **Email Service** under **Tools** in the sidebar. The page has four tabs: **Overview**, **Domains**, **Activity**, and **Settings**. The **Overview** tab shows a three-step checklist (**Add a sending domain**, **Create a token**, **Send a test email**) and cards for **Emails today**, **Delivery rate**, **Bounce rate**, and **Active domains**. ## Add a sending domain ### Open the Add domain dialog In **Email Service**, click **Add domain** on the **Overview** or **Domains** tab. ### Enter your company domain Under **Company domain**, enter a domain you control, such as `acme.com`, with no email address or URL path. You'll need access to its DNS settings. ### Choose a sending subdomain Under **Sending subdomain**, enter a single label. The default is `email`, which gives the sending domain `email.acme.com`. Choose a name that is not already used for a website or mailbox, such as `email`, `notify`, or `updates`. The dialog shows an example From address, such as `orders@email.acme.com`. You choose the real sender names later, when you register senders. ### Continue to DNS setup Click **Continue to DNS setup**. The domain opens on its own page with four setup steps: **Choose domain**, **Add DNS records**, **Verify connection**, and **Register sender**. If the sending domain already exists in the workspace, the dialog links to it so you can continue its setup. The **Domains** tab lists every sending domain with its **Status**, **Next action**, and creation date. You can search by name and filter by **All domains**, **Verified domains**, or **Needs setup or attention**. Click **Continue setup** or **Manage** to open a domain. ## Add NS records and verify > **Warning** > > Delegate only the sending subdomain. Do not change your company domain's nameservers or its existing MX records. Delegating a subdomain moves DNS for that subdomain to IBEE, so use a subdomain that nothing else depends on. ### Wait for the records When the domain is created, IBEE prepares its nameservers. The **Add DNS records** step says so while it waits, and the records appear automatically when ready. You don't need to add anything yet. ### Add the NS records at your DNS provider Open the DNS settings for your company domain and add each **NS** record exactly as shown. The table lists **Type**, **Name**, **Value**, and **Status**, with a copy button next to each name and value. **Copy setup instructions** copies all records and instructions in one step. * Copy **Name** and **Value** exactly. * Some DNS providers add your company domain to the name automatically. If yours does, don't enter it twice. * Leave TTL at your provider's default unless instructed otherwise. Click **I've added the records** when you're done. ### Verify the connection In the **Verify connection** step, click **Verify connection**. IBEE checks that the subdomain is delegated, then configures email authentication. The page updates automatically while checks run. DNS changes can take time to appear. If a check has not passed yet, you can leave the page and return to the domain later. If the step shows **Retry setup**, click it after fixing the issue described on the page. To see the authentication records IBEE manages, expand **What does IBEE configure?** in the **Verify connection** step. It lists the SPF, DKIM, DMARC, and bounce records inside the delegated subdomain. They are for reference only. Do not copy them into your company domain's DNS. ### Domain status | Status | What it means | What to do | | -------------------------- | ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | | **Setting up** | IBEE is preparing the domain. | Wait. | | **Needs DNS setup** | The NS records are ready to add. | Add the NS records at your DNS provider, then click **Verify connection**. | | **Checking nameservers** | IBEE is checking the delegation. | Wait. The page updates automatically. | | **Configuring email** | Delegation passed. IBEE is setting up email authentication. | Wait. | | **Checking email records** | IBEE is verifying the authentication records. | Wait. | | **Retrying** | A temporary problem occurred and IBEE is retrying. | Wait. If it persists, contact support. | | **Action required** | Setup stopped and needs your input. | Read the message on the domain page, fix the issue (usually the NS records), then click **Retry setup**. | | **Sandbox ready** | Verified for sandbox sending. | Register senders. Sandbox restrictions apply (see [Limits and sandbox](#limits-and-sandbox)). | | **Active** | DNS and email authentication are verified. | Register senders and create an Email Token. | | **Setup expired** | Setup was not completed in time. | Remove the domain and add it again, or contact support. | | **Suspended** | Sending from this domain is stopped. | Contact support. | | **Cancelled** | Setup was cancelled. | Add the domain again if you still need it. | | **Removing** | The domain is being removed. | Wait. | Each NS record also has its own status: **Pending**, **Checking**, **Verified**, **Missing**, or **Incorrect**. **Missing** or **Incorrect** means the record at your DNS provider doesn't match the value shown. ### Remove a domain Open the domain and click **Remove**, then type the domain name to confirm. New sends from the domain stop before its resources are released, and the domain shows **Removing**. Afterwards, delete the subdomain's NS records at your DNS provider. ## Register senders A sender is the exact From address your application is allowed to use. Registering a sender does not create an inbox. The API rejects any From address that is not a registered sender on an active domain. ### Open the domain When a domain is **Active** or **Sandbox ready**, open it and go to the **Register sender** step. ### Add the sender Click **Add sender** and fill in: * **Domain**: the verified sending domain. * **Sender name**: only the part before `@`, such as `orders`, `support`, or `notifications`, up to 64 characters. The domain is added automatically. * **Display name** (optional): for example, `Acme Orders`. * **Reply-To** (optional): for example, `support@acme.com`. The domain's sender table shows each **Address**, **Display name**, **Reply-To**, and **Status**. To set the display name and Reply-To on a message sent through the API, include `fromName` and `replyTo` in the request (see [Send an email](#send-an-email)). To remove a sender, click the delete icon on its row. Applications can no longer send from that address. ## Create an Email Token Applications authenticate to the send API with an **Email Token**. Each token can only send email from one verified sending domain. It cannot manage infrastructure or change Email Service settings. Platform API tokens cannot send email. ### Open Email Tokens In the sidebar, click **API Tokens** under **Organization**, then open the **Email Tokens** tab. You can also click **Create token** on the Email Service **Overview** tab or on a verified domain's page. Both open the same dialog with the workspace and domain preselected. ### Create the token Click **Create Email Token** and fill in: * **Token name**: for example, `production-app` (up to 80 characters). * **Workspace**: the workspace that owns the sending domain. * **Sending domain**: one verified domain in that workspace. * **Expiration**: **No expiration**, **7 days**, **30 days**, **90 days** (default), or **1 year**. The summary shows the sending domain, the permission (**Send email only**), and the expiry. Click **Create token**. ### Save the token Copy the token from the **Save this email token now** dialog. It is shown once and cannot be recovered later. Store it in your application server's secret store, never in frontend code. Only organization admins can create or revoke Email Tokens, and their account must be verified. To revoke a token, click **Revoke** on its row. Applications using it stop sending immediately. Messages that were already accepted are not cancelled. For platform API tokens and other credential types, see [API Tokens](/docs/tools/api-tokens). ## Send an email Send one message per request: ```http POST https://api.ibee.email/v1/emails ``` ### Headers | Header | Required | Value | | ----------------- | -------- | ------------------------------------------------------------------------------------------- | | `Authorization` | Yes | `Bearer ` | | `Content-Type` | Yes | `application/json` | | `Idempotency-Key` | Yes | A unique key for this message: 8–128 characters of letters, digits, `.`, `_`, `:`, and `-`. | ### Body | Field | Required | Description | | ---------- | ----------------------- | --------------------------------------------------------------- | | `from` | Yes | A registered sender address on a domain the token can use. | | `to` | Yes | One recipient email address, as a string. | | `subject` | Yes | 1–200 characters. | | `text` | One of `text` or `html` | Plain-text body, up to 100,000 characters. | | `html` | One of `text` or `html` | HTML body, up to 250,000 characters. | | `fromName` | No | Display name shown with the From address, up to 120 characters. | | `replyTo` | No | Reply-To email address. | Each request has exactly one recipient. To email several people, send one request per recipient, each with its own `Idempotency-Key`. Attachments, CC, and BCC are not supported. ### Examples **`cURL`** ```bash title="cURL" curl https://api.ibee.email/v1/emails \ -H "Authorization: Bearer $IBEE_EMAIL_TOKEN" \ -H "Idempotency-Key: order-1042-confirmation" \ -H "Content-Type: application/json" \ -d '{ "from": "orders@email.acme.com", "fromName": "Acme Orders", "to": "buyer@example.com", "replyTo": "support@acme.com", "subject": "Your order is confirmed", "text": "Thank you for your order.", "html": "

Thank you for your order.

" }' ``` **`Python`** ```python title="Python" import os import requests response = requests.post( "https://api.ibee.email/v1/emails", headers={ "Authorization": f"Bearer {os.environ['IBEE_EMAIL_TOKEN']}", "Idempotency-Key": "order-1042-confirmation", }, json={ "from": "orders@email.acme.com", "fromName": "Acme Orders", "to": "buyer@example.com", "subject": "Your order is confirmed", "text": "Thank you for your order.", }, timeout=30, ) response.raise_for_status() print(response.json()["id"]) ``` **`Node.js`** ```javascript title="Node.js" const response = await fetch("https://api.ibee.email/v1/emails", { method: "POST", headers: { Authorization: `Bearer ${process.env.IBEE_EMAIL_TOKEN}`, "Idempotency-Key": "order-1042-confirmation", "Content-Type": "application/json", }, body: JSON.stringify({ from: "orders@email.acme.com", fromName: "Acme Orders", to: "buyer@example.com", subject: "Your order is confirmed", text: "Thank you for your order.", }), }); const result = await response.json(); if (!response.ok) { throw new Error(`${result.error.code}: ${result.error.message}`); } console.log(result.id); ``` ### Response A successful request returns `202 Accepted`. This means the message was accepted and queued. Delivery happens afterwards and is reported in **Activity** and through webhooks. ```json { "id": "msg_Xk3v9Qp2LmN7tRzA", "status": "accepted", "duplicate": false } ``` ### Idempotency The `Idempotency-Key` makes retries safe. If a request fails because of a timeout or network error, retry it with the same key. When a key has already been used in the workspace, the API returns the original message's `id` with `"duplicate": true` and does not send it again. Derive the key from your own record, such as `order-1042-confirmation`. Use a new key for every distinct email. A reused key never sends a second message, even if the body changed. ### Errors Errors return a JSON body with a machine-readable `code`: ```json { "error": { "code": "recipient_suppressed", "message": "Recipient is on this workspace's suppression list." } } ``` | Status | Code | Meaning | | ------ | --------------------------------------------------------- | ------------------------------------------------------------------------- | | 400 | `idempotency_key_required` | The `Idempotency-Key` header is missing. | | 400 | `idempotency_key_invalid` | The key is not 8–128 allowed characters. | | 400 | `invalid_json` | The body is not a valid JSON object. | | 400 | `sender_invalid`, `recipient_invalid`, `reply_to_invalid` | An email address is malformed. | | 400 | `subject_invalid` | The subject is empty or longer than 200 characters. | | 400 | `body_empty` | Neither `text` nor `html` was provided. | | 401 | `api_key_required` | The `Authorization: Bearer` header is missing. | | 401 | `api_key_invalid` | The token is wrong, revoked, or expired. | | 403 | `sender_not_allowed` | The From address is not a registered sender on an active domain. | | 403 | `sender_not_allowed_by_key` | The token is not allowed to send from this sender's domain. | | 403 | `scope_missing` | The token does not have send permission. | | 403 | `tenant_inactive` | Email Service is not active for this workspace. | | 409 | `recipient_suppressed` | The recipient is on the workspace's suppression list. | | 413 | `body_too_large`, `request_too_large` | The content or the whole request is too large. | | 415 | `content_type_invalid` | `Content-Type` is not `application/json`. | | 429 | `daily_limit_reached` | The workspace's daily limit is used up. `details.limit` shows the limit. | | 429 | `quota_or_acceptance_conflict` | The message could not be accepted. Retry with the same `Idempotency-Key`. | | 500 | `internal_error` | Unexpected error. Retry with the same `Idempotency-Key`. | Every response includes an `x-request-id` header. Include it when you contact support. ### Send a test from the portal To check your setup without writing code, click **Send test** in the **Overview** checklist or **Send test email** on the **Activity** tab. Choose a registered sender under **From**, enter the **To**, **Subject**, and **Message**, then click **Send**. The result appears in **Activity**. Test emails are real messages and count toward the daily limit. ## Activity and message timeline The **Activity** tab lists recent messages with **From**, **To**, **Subject**, **Status**, and **Created**. Click **Refresh** to load new results, and **Load older messages** to page back. The search box filters the messages already loaded on the page by ID, address, subject, or status. Click a message's subject to open its details. They show the message ID, From, To, and Subject, plus a timeline of every delivery event with its time, a description, and the SMTP status code when the recipient's mail server returned one. | Status | Meaning | | ------------- | -------------------------------------------------------------------------------------------------------- | | **Accepted** | The API accepted the message. | | **Queued** | Waiting to be sent. | | **Submitted** | Handed to the recipient's mail server. | | **Deferred** | Temporarily refused. Delivery is being retried. | | **Delivered** | The recipient's mail server accepted the message. | | **Bounced** | The recipient's mail server permanently rejected the message. The recipient is suppressed automatically. | | **Rejected** | The message was rejected and will not be delivered. | | **Failed** | The message could not be sent. | | **Complaint** | The recipient marked the message as spam. The recipient is suppressed automatically. | ## Suppressions The suppression list holds recipients this workspace will not send to. A request to a suppressed recipient fails with `409 recipient_suppressed`. To open it, click **Suppressed recipients** on the **Activity** tab. Each entry shows the **Recipient**, **Reason**, and the date it was **Added**. | Reason | Added by | Can you remove it? | | --------------- | -------------------------------------------- | -------------------------------------- | | **Manual** | You, with **Add suppression** | Yes. Click the delete icon on its row. | | **Hard bounce** | Automatically, when a message bounces | No. It shows as **Automatic**. | | **Complaint** | Automatically, when a recipient reports spam | No. It shows as **Automatic**. | To block an address yourself, click **Add suppression**, enter the email address, and confirm. If an automatic suppression was added in error, contact support. ## Webhooks Webhooks send signed delivery events to an HTTPS endpoint in your application. They are optional. You host the receiver. To add an endpoint: ### Open Webhooks In **Email Service**, open the **Settings** tab and expand **Webhooks**. ### Save the endpoint URL Under **Webhook endpoint**, keep **Add a new endpoint** selected. Enter your **Endpoint URL**, such as `https://api.example.com/webhooks/email`, and click **Save webhook**. The URL must be a public HTTPS address with no username, password, or `#fragment`. Local and private addresses are rejected. ### Copy the signing secret The signing secret is shown once, right after you save. Copy it into your server's configuration, then click **I've saved the secret**. ### Send a test event Click **Send test event**. IBEE sends a synthetic `email.delivered` event with `"test": true` in its `data`. No email is sent. Click **Refresh status** to see the result. New endpoints receive all six event types. To manage an existing endpoint, choose it from **Webhook endpoint**. Its status is **Active**, **Disabled**, or **Failing**, and the panel shows the latest delivery result. * **Update webhook**: change the endpoint URL. Subscriptions and status are kept. * **Disable webhook** or **Enable webhook**: stop or resume deliveries to the endpoint. * **Rotate secret**: issue a new signing secret. The old secret stops working immediately, so update your receiver right away. ### Events | Event | Sent when | | ------------------ | ------------------------------------------------------------------------ | | `email.delivered` | The recipient's mail server accepted the message. | | `email.deferred` | Delivery was temporarily refused and is being retried. | | `email.bounced` | The message bounced permanently. The recipient is suppressed. | | `email.rejected` | The message was rejected and will not be delivered. | | `email.failed` | The message could not be sent. | | `email.complained` | The recipient reported the message as spam. The recipient is suppressed. | Each delivery is a `POST` with a JSON body: ```json { "id": "evt_4fT8wQm1ZkP0sVnB", "type": "email.bounced", "createdAt": "2026-10-01T09:30:12.000Z", "data": { "messageId": "msg_Xk3v9Qp2LmN7tRzA", "from": "orders@email.acme.com", "to": "buyer@example.com", "subject": "Your order is confirmed", "status": "bounced", "smtpStatusCode": "550", "smtpResponse": "5.1.1 User unknown" } } ``` `data.messageId` matches the `id` returned by `POST /v1/emails`. Payloads can include more fields. Ignore any you don't use. ### Delivery and retries * Return any `2xx` status within 15 seconds to acknowledge an event. * Redirects are not followed. * Timeouts, network errors, and `408`, `425`, `429`, and `5xx` responses are retried with increasing delays. Other responses are not retried. * Retries can deliver the same event more than once. Deduplicate with the `x-ibee-event-id` header. ### Verify signatures Every delivery includes three headers: | Header | Value | | ------------------ | ----------------------------------------------- | | `x-ibee-signature` | `v1=` followed by the hex HMAC-SHA256 signature | | `x-ibee-timestamp` | The signing time, in Unix seconds | | `x-ibee-event-id` | The event ID, for deduplication | To verify a delivery: 1. Compute HMAC-SHA256 with your signing secret over the exact string `timestamp + "." + rawRequestBody`. Use the raw bytes you received, not re-serialized JSON. 2. Compare the hex digest with the value after `v1=` using a constant-time comparison. 3. Reject timestamps more than 5 minutes from your server's clock. **`Python (Flask)`** ```python title="Python (Flask)" import hashlib import hmac import os import time from flask import Flask, abort, request app = Flask(__name__) SECRET = os.environ["IBEE_WEBHOOK_SECRET"].encode() TOLERANCE_SECONDS = 300 @app.post("/webhooks/email") def ibee_email_webhook(): raw_body = request.get_data() timestamp = request.headers.get("x-ibee-timestamp", "") signature = request.headers.get("x-ibee-signature", "") if not timestamp.isdigit() or abs(time.time() - int(timestamp)) > TOLERANCE_SECONDS: abort(400) expected = hmac.new(SECRET, timestamp.encode() + b"." + raw_body, hashlib.sha256).hexdigest() if not signature.startswith("v1=") or not hmac.compare_digest(signature[3:], expected): abort(400) event = request.get_json() # Skip events whose x-ibee-event-id you have already processed. print(event["type"], event["data"].get("messageId")) return "", 204 ``` **`Node.js (Express)`** ```javascript title="Node.js (Express)" import crypto from "node:crypto"; import express from "express"; const app = express(); const SECRET = process.env.IBEE_WEBHOOK_SECRET; const TOLERANCE_SECONDS = 300; app.post("/webhooks/email", express.raw({ type: "application/json" }), (req, res) => { const timestamp = req.get("x-ibee-timestamp") ?? ""; const signature = req.get("x-ibee-signature") ?? ""; const fresh = Math.abs(Date.now() / 1000 - Number(timestamp)) <= TOLERANCE_SECONDS; const expected = crypto .createHmac("sha256", SECRET) .update(`${timestamp}.`) .update(req.body) .digest("hex"); const received = signature.startsWith("v1=") ? signature.slice(3) : ""; const valid = received.length === expected.length && crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected)); if (!fresh || !valid) return res.sendStatus(400); const event = JSON.parse(req.body.toString("utf8")); // Skip events whose x-ibee-event-id you have already processed. console.log(event.type, event.data.messageId); res.sendStatus(204); }); app.listen(3000); ``` Keep the signing secret on your server. Never put it in frontend code. ## Limits and sandbox Workspaces start with sandbox access. A domain that shows **Sandbox ready** is verified for sandbox sending, and sandbox recipient and quota restrictions apply. To ask about production access or a higher limit, contact [support](/docs/support/contact-support). Each workspace has a daily sending limit. The **Emails today** card on the **Overview** tab shows how many messages you have sent today and your daily limit. When the limit is reached, the API returns `429 daily_limit_reached` until the next day starts (00:00 UTC). Test emails sent from the portal count toward the limit. Repeated requests with an already-used `Idempotency-Key` do not. | Limit | Value | | ------------------------------- | ----------------------------------------------------- | | Recipients per request | 1 | | Subject | 1–200 characters | | `text` body | 100,000 characters | | `html` body | 250,000 characters | | Request body | 256 KB | | `Idempotency-Key` | 8–128 characters: letters, digits, `.`, `_`, `:`, `-` | | `fromName` | 120 characters (longer values are truncated) | | Sending domains per Email Token | 1 | | Sender name (part before `@`) | 64 characters | Keep your bounce rate low. The **Bounce rate** card on **Overview** targets below 2%. Send only to addresses that asked for your email, and respect the suppression list. ## Related * [API Tokens](/docs/tools/api-tokens): platform API tokens and the other credential types managed on the same page. * [Verify your identity](/docs/getting-started/account-setup/verify-your-identity): required before creating Email Tokens. * [Contact support](/docs/support/contact-support): request Email Service access, production access, or help with a domain. > Send transactional email from your own subdomain over an HTTPS API. Delegate a subdomain to IBEE, register sender addresses, send with a send-only Email Token, and track delivery with activity, suppressions, and signed webhooks.