Skip to article
InboxNest
← InboxNest home

Send app emails with the InboxNest API

Send password resets, receipts and notifications from your app through InboxNest, or switch from Resend by changing the base URL.

The transactional API sends one-way email from your own app: password resets, receipts, sign-up confirmations and notifications. These emails don’t create inbox conversations, go out on their own sending stream, and have their own limits.

Transactional or conversations?

  • Transactional (POST /emails): your app emails a user because of something they did. No reply is expected in InboxNest. Any From address on your domain works, including noreply@.
  • Conversations (POST /conversations/{id}/reply and POST /conversations): support mail that should appear in your inbox, where the customer’s reply lands in the same thread.

Transactional email is for transactional messages only. Marketing, newsletters and cold email aren’t allowed, and recipients that bounced or complained may be blocked.

Getting access

In Settings → API keys, use Request access next to “Send app emails”. Tell us your app’s URL, what it sends and roughly how many emails a month, and confirm it is transactional only. Once approved, create a key with mail:transactional. This doesn’t depend on Allow AI apps to send replies: that switch is about AI in your inbox, and this is your app’s own mail.

Verified sending domains

The From address must be on one of your InboxNest domains with DKIM and the custom Return-Path verified (the domain shows can_send: true in GET /domains). It must also be a domain the key is allowed to use. Use "Name <local@yourdomain.com>" or a bare address.

Send an email

POST https://tryinboxnest.com/api/v1/emails with an Idempotency-Key header:

{
  "from": "Acme <noreply@acme.com>",
  "to": ["jo@example.com"],
  "cc": [], "bcc": [], "reply_to": "help@acme.com",
  "subject": "Reset your password",
  "html": "<p>Click the link to reset your password.</p>",
  "text": "Click the link to reset your password.",
  "headers": {"X-Entity-Ref-ID": "reset-881"},
  "attachments": [{"filename": "receipt.pdf", "content": "JVBERi0x…", "content_type": "application/pdf"}],
  "tags": [{"name": "category", "value": "password_reset"}],
  "metadata": {"user_id": "881"}
}

It returns 202 and {"id": "eml_…", "status": "sent", "recipients": 1, "suppressed": []}.

  • At least one of html and text; each up to 100 KB. The subject can be up to 255 characters.
  • Up to 50 recipients in total across to, cc and bcc.
  • Attachments are base64 and can total 10 MB. Executable types (.exe, .bat, .cmd, .js, .vbs, .scr, .msi, .jar and similar) are refused.
  • Custom headers: List-Unsubscribe, List-Unsubscribe-Post, X-Entity-Ref-ID and other X- headers only.
  • POST /emails/batch takes {"emails": [ … ]} with up to 100 emails. Each one is checked and sent on its own, so some can succeed while others fail. Each result is either the send result or {"index": 1, "error": {…}}.

Examples

curl

curl -X POST https://tryinboxnest.com/api/v1/emails \
  -H "Authorization: Bearer $INBOXNEST_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: reset-881" \
  -d '{"from":"Acme <noreply@acme.com>","to":"jo@example.com","subject":"Reset your password","text":"Use this link…"}'

Node

const res = await fetch('https://tryinboxnest.com/api/v1/emails', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.INBOXNEST_KEY}`,
    'Content-Type': 'application/json',
    'Idempotency-Key': `reset-${user.id}-${Date.now()}`,
  },
  body: JSON.stringify({ from: 'Acme <noreply@acme.com>', to: user.email, subject: 'Reset your password', html }),
});
const { id } = await res.json();

Python

import os, requests
r = requests.post(
    "https://tryinboxnest.com/api/v1/emails",
    headers={"Authorization": f"Bearer {os.environ['INBOXNEST_KEY']}", "Idempotency-Key": f"reset-{user_id}"},
    json={"from": "Acme <noreply@acme.com>", "to": email, "subject": "Reset your password", "text": body},
    timeout=15,
)
r.raise_for_status()
print(r.json()["id"])

PHP

$ch = curl_init('https://tryinboxnest.com/api/v1/emails');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('INBOXNEST_KEY'),
        'Content-Type: application/json',
        'Idempotency-Key: reset-' . $userId,
    ],
    CURLOPT_POSTFIELDS => json_encode(['from' => 'Acme <noreply@acme.com>', 'to' => $email, 'subject' => 'Reset your password', 'text' => $body]),
]);
$response = json_decode(curl_exec($ch), true);

Moving from Resend

InboxNest accepts Resend’s request and response shapes at https://tryinboxnest.com/api/resend for POST /emails, POST /emails/batch and GET /emails/{id}. Use an InboxNest API key with mail:transactional in place of your Resend key, and make sure your From domain is verified in InboxNest.

Node (the official resend package accepts a baseUrl option, or reads RESEND_BASE_URL):

import { Resend } from 'resend';

const resend = new Resend(process.env.INBOXNEST_KEY, { baseUrl: 'https://tryinboxnest.com/api/resend' });
const { data, error } = await resend.emails.send({
  from: 'Acme <noreply@acme.com>',
  to: ['jo@example.com'],
  subject: 'Reset your password',
  html: '<p>…</p>',
});

Python (the official resend package reads resend.api_url, or the RESEND_API_URL environment variable):

import os
import resend

resend.api_key = os.environ["INBOXNEST_KEY"]
resend.api_url = "https://tryinboxnest.com/api/resend"
resend.Emails.send({"from": "Acme <noreply@acme.com>", "to": ["jo@example.com"], "subject": "Reset your password", "html": "<p>…</p>"})

What’s different:

  • Not supported: scheduled sending (scheduled_at), React rendering (react), templates, topics, audiences, broadcasts, the domains API, attachments by path (URL) and inline images. Each returns a 422 that names the feature; nothing is silently dropped.
  • Batch: Resend rejects a whole batch if one email is invalid. InboxNest sends the valid ones and lists the rest in errors (index, message).
  • Ids look like eml_…, and GET /emails/{id} returns no html or text, because InboxNest doesn’t store bodies.
  • Errors use Resend’s shape: {"statusCode": 422, "name": "validation_error", "message": "…"}.

Limits and warm-up

  • Monthly allowance: Starter 1,000, Business 5,000, Portfolio 10,000 emails per billing period; trials 100 a day. Each recipient counts as one email. We email you at 80% and 100%. At 100% the API returns 429 monthly_limit_reached until the next period; contact support if you need more.
  • Warm-up: a new approval starts at 200 emails a day. The limit doubles every 7 days while bounces stay under 2% and complaints under 0.1% (with at least 200 sends), up to your plan’s allowance. A spike in complaints resets it.
  • Automatic suspension: a bounce rate over 5% (at least 50 sends) in 7 days, a complaint rate over 0.1% (at least 500 sends) or 3 complaints in 7 days, or 20 or more sends to suppressed addresses in a day suspends app email for the account until we review it. Replies to customers aren’t affected.

Suppressions

Recipients on your suppression list, or that hard-bounced or marked InboxNest mail as spam anywhere in InboxNest, are skipped. The response lists them in suppressed. If every recipient is suppressed, nothing is sent and the status is suppressed. A bounce or complaint on your app email adds that address to your suppression list.

Status and events

GET /emails/{id} returns the status (queued, sent, delivered, bounced, complained, failed or suppressed), each recipient’s status and the list of events. GET /emails lists recent emails with status, domain_id, to and since filters. Webhooks send email.sent, email.delivered, email.bounced, email.complained and email.failed, with email_id, to, subject, tags and metadata. Settings → Email log shows the last 30 days.