API Documentation
Everything you need to send emails via our REST API
Quick Start
- Create an account
- Create an API key with your SMTP credentials in the Dashboard
- Copy the API key (starts with
rk_) - shown only once! - Make HTTP POST requests to send emails
Base URL: https://2smtp.com
How It Works
You send HTTP request
POST to our API with your email data
We relay via YOUR SMTP
Using the credentials you configured
Email is delivered
Sent from your own mail server
Credit Counting
Each recipient counts as one credit, regardless of message size.
Example:
- 1 TO recipient = 1 credit
- 1 TO + 2 CC = 3 credits
- 5 TO + 3 CC + 2 BCC = 10 credits
Authentication
API Key (for sending emails)
Use your API key to send emails:
X-API-Key: rk_your_api_keySession Token (for key management)
Get a session token by logging in, then use it to manage API keys:
Authorization: Bearer your_session_tokenKey types: API keys (rk_) are for sending emails.
Master keys (mk_) are for programmatic account management.
Send Email API
Requires your sending key, either as X-API-Key: rk_... or Authorization: Bearer rk_... β both work.
/api/v1/sendSend an email using your API key.
Request Body
{
"to": ["recipient@example.com"], // Required: array of recipient emails
"cc": ["cc@example.com"], // Optional: array of CC emails
"bcc": ["bcc@example.com"], // Optional: array of BCC emails
"reply_to": "replyto@example.com", // Optional: reply-to address
"subject": "Email Subject", // Required: email subject
"body_text": "Plain text body", // Optional*: plain text version
"body_html": "<p>HTML body</p>" // Optional*: HTML version
}
// * At least one of body_text or body_html is requiredSuccess Response (200)
{
"success": true,
"recipients": 1,
"credits_remaining": 9999
}Error Response (4xx/5xx)
{
"error": "invalid_api_key"
}/api/v1/preflightGrade your sender domain's SPF/DKIM/DMARC/MX before spending credits. With no
params it checks your key's from-address domain β CI-friendly: curl -H "X-API-Key: rk_..." https://2smtp.com/api/v1/preflight.
Optional ?domain= and ?selector=.
/api_fe/send_test_email_fe Session authSend a test email to verify a key's SMTP configuration. Uses your dashboard
session (Authorization: Bearer), not the API key header.
{
"api_key_chirho": "rk_the_key_to_test",
"to_email_chirho": "you@example.com"
}/api_fe/usage_fe Session authGet current credit usage for your whole account (session or master-key Authorization: Bearer β not the API key header).
{
"total_credits_chirho": 10000,
"used_credits_chirho": 1,
"remaining_credits_chirho": 9999,
"total_emails_sent_chirho": 1
}Idempotent Retries
Networks fail mid-request. If your send times out, you cannot tell whether the email
went out β retrying blindly risks a duplicate, and giving up risks nothing being sent.
Pass an Idempotency-Key header and
2SMTP guarantees at most one successful, charged send per key: a retry
with the same key and body returns the original response instead of sending again.
curl -X POST https://2smtp.com/api/v1/send \
-H "X-API-Key: rk_your_api_key" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-42-receipt" \
-d '{ "to": ["customer@example.com"], "subject": "Receipt", "body_text": "Thanks!" }'How it behaves:
- First successful send is stored; a retry with the same key and body gets the stored
response back with an
Idempotent-Replayed: trueheader β no second send, no second charge. - Same key with a different body returns
422 idempotency_key_reusedβ pick a new key for a new message. - If the original request is still in flight, a concurrent retry gets
409 idempotency_in_flightwith aRetry-Afterheader. - Failed sends are not stored β a retry after an error executes fresh, so a transient relay problem never locks your key.
- Keys are scoped to your API key, up to 255 characters (an order ID or UUID works well), and are retained for 24 hours.
Key Management API
Requires Authorization: Bearer with either a dashboard session token or a
master key (mk_...) for automation.
/api_fe/keys_feCreate a new API key with SMTP configuration.
curl -X POST https://2smtp.com/api_fe/keys_fe \
-H "Authorization: Bearer your_session_token" \
-H "Content-Type: application/json" \
-d '{
"smtp_host_chirho": "smtp.gmail.com",
"smtp_port_chirho": 587,
"smtp_user_chirho": "your-email@gmail.com",
"smtp_pass_chirho": "your-app-password",
"smtp_tls_chirho": true,
"from_email_chirho": "your-email@gmail.com",
"label_chirho": "Production"
}'/api_fe/keys_feList all API keys with details.
curl https://2smtp.com/api_fe/keys_fe \
-H "Authorization: Bearer your_session_token"/api_fe/update_key_label_feUpdate the label for an API key.
/api_fe/keys_fe/{api_key}Read one key, update its SMTP configuration (PATCH accepts the same fields as create), or delete it permanently.
/api_fe/deactivate_key_feDeactivate an API key permanently.
/api_fe/test_smtp_feTest SMTP connection before creating a key.
Credits & Usage API
Requires session token authentication.
/api_fe/credits_feGet credit pool balance.
curl https://2smtp.com/api_fe/credits_fe \
-H "Authorization: Bearer your_session_token"/api_fe/usage_stats_feGet daily and per-key usage statistics (last 30 days).
{
"daily_usage_chirho": [{ "date_chirho": "2026-07-16", "emails_chirho": 120 }],
"total_30_days_chirho": 1830,
"key_stats_chirho": [{ "api_key_preview_chirho": "rk_9c5c...", "label_chirho": "Production", "emails_sent_chirho": 1830 }]
}/api_fe/history_fePaginated send history β every attempt that reached your SMTP server, including
failures. Query params: page, limit (default 50, max
100), status (sent | failed), and key; the response carries pagination_chirho metadata.
/api_fe/checkout_feCreate a Stripe checkout session to purchase credits.
Master Keys API
Master keys allow programmatic management of your 2SMTP account for automation and AI integration.
Honest note: scopes are recorded for audit; enforcement is not yet active β treat every master key as full-account access when you share one.
/api_fe/master_keys_feCreate a new master key with optional scopes and expiration.
/api_fe/master_keys_feList all master keys (shows preview only, not full key).
/api_fe/master_keys_feRevoke a master key by its preview.
Code Examples
cURL
curl -X POST https://2smtp.com/api/v1/send \
-H "X-API-Key: rk_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"to": ["recipient@example.com"],
"subject": "Hello from 2SMTP!",
"body_text": "This is a plain text email.",
"body_html": "<h1>Hello!</h1><p>This is an HTML email.</p>"
}'JavaScript / TypeScript
const response = await fetch('https://2smtp.com/api/v1/send', {
method: 'POST',
headers: {
'X-API-Key': 'rk_your_api_key',
'Content-Type': 'application/json'
},
body: JSON.stringify({
to: ['recipient@example.com'],
subject: 'Hello from 2SMTP!',
body_text: 'This is a plain text email.',
body_html: '<h1>Hello!</h1><p>This is an HTML email.</p>'
})
});
const data = await response.json();
console.log(data.success); // true
console.log(data.recipients); // 1
console.log(data.credits_remaining); // 9999Python
import requests
response = requests.post(
'https://2smtp.com/api/v1/send',
headers={
'X-API-Key': 'rk_your_api_key',
'Content-Type': 'application/json'
},
json={
'to': ['recipient@example.com'],
'subject': 'Hello from 2SMTP!',
'body_text': 'This is a plain text email.',
'body_html': '<h1>Hello!</h1><p>This is an HTML email.</p>'
}
)
data = response.json()
print(data['success']) # True
print(data['recipients']) # 1
print(data['credits_remaining']) # 9999PHP
<?php
$ch = curl_init('https://2smtp.com/api/v1/send');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'X-API-Key: rk_your_api_key',
'Content-Type: application/json'
],
CURLOPT_POSTFIELDS => json_encode([
'to' => ['recipient@example.com'],
'subject' => 'Hello from 2SMTP!',
'body_text' => 'Sent from PHP with one HTTP call.'
])
]);
$data = json_decode(curl_exec($ch), true);
curl_close($ch);
echo $data['success'] ? 'sent' : $data['error'];
echo $data['credits_remaining'];Go
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)
func main() {
payload, _ := json.Marshal(map[string]any{
"to": []string{"recipient@example.com"},
"subject": "Hello from 2SMTP!",
"body_text": "Sent from Go with one HTTP call.",
})
req, _ := http.NewRequest("POST", "https://2smtp.com/api/v1/send", bytes.NewReader(payload))
req.Header.Set("X-API-Key", "rk_your_api_key")
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
var data struct {
SuccessChirho bool `json:"success"`
RecipientsChirho int `json:"recipients"`
CreditsRemainingChirho int `json:"credits_remaining"`
}
json.NewDecoder(resp.Body).Decode(&data)
fmt.Println(data.SuccessChirho, data.CreditsRemainingChirho)
}Ruby
require 'net/http'
require 'json'
uri = URI('https://2smtp.com/api/v1/send')
request = Net::HTTP::Post.new(uri)
request['X-API-Key'] = 'rk_your_api_key'
request['Content-Type'] = 'application/json'
request.body = {
to: ['recipient@example.com'],
subject: 'Hello from 2SMTP!',
body_text: 'Sent from Ruby with one HTTP call.'
}.to_json
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
http.request(request)
end
data = JSON.parse(response.body)
puts data['success']
puts data['credits_remaining']Migrating from Nodemailer
// Before: nodemailer needs an SMTP connection from your runtime -
// a problem on serverless platforms with no raw TCP (Workers, Edge).
//
// const transporter = nodemailer.createTransport({ host, port, auth });
// await transporter.sendMail({ from, to, subject, text });
// After: same SMTP server, but 2SMTP holds the connection - your code
// is one fetch that works anywhere JavaScript runs.
await fetch('https://2smtp.com/api/v1/send', {
method: 'POST',
headers: { 'X-API-Key': 'rk_your_api_key', 'Content-Type': 'application/json' },
body: JSON.stringify({
to: ['recipient@example.com'],
subject: 'Hello from 2SMTP!',
body_text: 'No SMTP client library required.'
})
});Rate Limits
Important: Rate limits protect your SMTP server's reputation. Sending too fast can trigger spam filters and blacklisting.
- 50 emails per minute per API key
- 1,000 recipients per single request (TO + CC + BCC combined)
When you hit a limit
The response is 429 with a Retry-After header (seconds) and
a JSON body naming the limit:
{
"error": "rate_limit_exceeded",
"retry_after_seconds": 30
}Error Codes
| Status | Meaning |
|---|---|
| 400 | Bad request - invalid parameters |
| 401 | Unauthorized - invalid or missing API key |
| 402 | Insufficient credits (insufficient_credits) β nothing was sent or charged |
| 404 | Resource not found |
| 409 | Idempotent request still in flight (idempotency_in_flight) β retry after Retry-After |
| 422 | Idempotency key reused with a different body, or SMTP configuration rejected |
| 429 | Rate limit or per-key cap exceeded (rate_limit_exceeded, daily_limit_exceeded, total_limit_exceeded) |
| 500 | Server error |
| 502 | Relay error while sending (satellite_error) β credits released, retry safe |
| 503 | No healthy relay or SMTP connection failed (smtp_connect_failed, smtp_timeout, ...) |
Every error body carries a machine-readable error code (snake_case,
as shown above) β branch on it, not on the human-readable message.
Common SMTP Providers
When creating an API key, you'll need your SMTP credentials. Here are settings for popular providers:
Gmail / Google Workspace
smtp.gmail.com Port:587 (TLS) or 465 (SSL) Username:your-email@gmail.com Password:App Password (not your regular password)Outlook / Microsoft 365
smtp.office365.com Port:587Amazon SES
email-smtp.[region].amazonaws.com Port:587SendGrid
smtp.sendgrid.net Port:587 Username:apikey Password:Your SendGrid API keyOpenAPI Specification
Full machine-readable API documentation in OpenAPI 3.0.3 format.
MCP Integration (Coming Soon)
MCP server integration for AI assistants like Claude Desktop is planned.
remail_send_email- Send an email via API keyremail_list_keys- List all API keysremail_get_usage- View usage statistics
Need Help?
Having trouble integrating? Contact us or use the feedback balloon on any page β feedback lands straight on our workbench.