Pengguna baharuPelan DNS dan CDN percuma untuk setiap akaun baharu.Cipta akaun anda

SMTP and HTTP API

Create API keys and send mail over SMTP or the HTTP API, with request fields, responses, error codes and examples.

9 minit bacaan Dikemas kini 20 Okt 2026

You can send through a stream in two ways: over SMTP from any application or mail library, or with the HTTP API. Both use the same API keys, quotas, suppression list and events. Before you start, verify a sending domain and create an API key.

API keys

  1. Open the stream and go to the API key tab.
  2. Click Create key, enter a Name such as production, and confirm.
  3. Copy the key that's shown and click I've saved it. The key is shown only this once. Afterwards the list shows only its Prefix, such as ytm_aB3dE6gH….

Each key starts with ytm_. A stream can have up to 20 active keys. The list shows when each key was last used. To retire a key, click Revoke: programs using it stop working immediately. A key works only for the stream it belongs to.

Warning

Keep API keys on your server. Don't put them in browser code, mobile apps or public repositories. Anyone with the key can send mail as your domain.

SMTP

The Overview tab of the stream shows the SMTP settings:

SettingValue
HostThe SMTP host shown for the stream, usually smtp.tm.yeapcloud.net
Port587 with STARTTLS, or 465 with TLS
Usernameapikey (literally this word)
PasswordAn API key of this stream
Category headerX-Yeap-Category

Use the host shown in the console. Some streams send through a different server group with its own host name.

How SMTP sending works:

  • The From header must use an address at a domain verified for the stream.
  • Each recipient (To, Cc and Bcc) gets its own copy and its own message ID, and counts as one email.
  • To set a category, add a header such as X-Yeap-Category: password-reset. It's removed before delivery. Allowed characters are letters, digits, ., _ and -, up to 64 characters.
  • A message can be up to 25 MB, with up to 1,000 recipients per SMTP transaction.
  • Every message gets an X-TM-ID header (the message ID that appears in the sending log and in events) and a Feedback-ID header for mailbox providers' feedback loops.
  • On marketing streams, any List-Unsubscribe header you set is replaced with Yeap Cloud's one-click unsubscribe headers. SMTP doesn't change the message body, so add your postal address and an unsubscribe link to the content yourself. See Suppressions and unsubscribe.
  • Open and click tracking are only available through the HTTP API.

Example with Python's standard library:

python
import smtplib
from email.message import EmailMessage

msg = EmailMessage()
msg["From"] = "Example <noreply@example.com>"
msg["To"] = "user@example.org"
msg["Subject"] = "Reset your password"
msg["X-Yeap-Category"] = "password-reset"
msg.set_content("Click the link to reset your password: https://example.com/reset/abc")

with smtplib.SMTP("smtp.tm.yeapcloud.net", 587) as s:
    s.starttls()
    s.login("apikey", "ytm_your_api_key")
    s.send_message(msg)

SMTP response codes

When the server refuses a message, the reply code tells you why. 4xx codes are temporary: retry later. 5xx codes are permanent: fix the cause before sending again.

CodeMessageCause and what to do
535 5.7.8Authentication failed / API key revokedWrong username or key, or the key was revoked. The username must be apikey.
454 4.7.0Temporary authentication failure / Too many authentication failuresThe server isn't ready yet, or your IP failed to log in too many times. Wait and retry.
451 4.7.1Stream is pausedThe stream was paused automatically. See the reason on the stream and contact support.
550 5.7.1Stream is banned / suspended / terminatedThe stream was banned, its subscription is suspended, or it was released.
550 5.7.1From domain … is not verified for this streamVerify the domain, or check that it's shared with or bound to this stream.
550 5.7.1Sandbox mode: recipient is not a verified addressThe stream is in the sandbox. Verify the recipient or send to your own domain.
550 5.7.1Recipient is on a suppression list / All recipients are suppressedThe address is suppressed (bounce, complaint, unsubscribe or manual).
452 4.7.1Hourly sending limit reached, try laterThe hourly limit (or the sandbox daily limit) is used up. Retry in the next hour.
550 5.7.1Monthly sending quota exhaustedThe monthly volume is used up and overage isn't allowed: on the free plan, or when the overage spending cap is reached.
452 4.5.3Too many recipientsMore than 1,000 recipients in one transaction.
553 5.1.3Invalid recipient addressThe recipient address is malformed.
554 5.7.1Message rejected: virus detectedThe message contained a virus. The stream is banned immediately and all its API keys are revoked.
554 5.7.1Message rejected as spamThe content scored as spam. Repeated rejections pause the stream.
451 4.3.0Content scanner unavailable, try again laterMarketing mail is only accepted after scanning. Retry later. Other 451 4.3.0 replies are temporary server errors: retry.
554 5.6.0Malformed message headersFix the message headers.

HTTP API

ItemValue
Base URLhttps://api.tm.yeapcloud.net/v1 (also available as https://api.yeap.cloud/api/tm/v1)
AuthenticationAuthorization: Bearer ytm_your_api_key
FormatJSON request and response bodies, Content-Type: application/json
Rate limit600 requests per minute per API key

Errors return an HTTP status code and a body like this:

json
{"error": {"code": "invalid_request", "message": "subject is required (single line)"}}
HTTP statuserror.codeMeaning
400invalid_json, invalid_requestThe body isn't a JSON object, or a batch is empty or too large
401unauthorizedMissing, malformed, revoked or unknown API key
403stream_paused, stream_banned, stream_suspendedThe stream can't send
403from_domain_unverified, invalid_fromThe From domain isn't verified for this stream, or the From address is invalid
403postal_address_requiredMarketing stream without a postal address. Fill it in on the Marketing settings tab.
403virusAn attachment contained a virus. The stream is banned.
404not_foundThe message doesn't exist in this stream
422invalid_requestA field is missing or invalid. message says which one.
429rate_limitedMore than 600 requests in a minute. Slow down and retry.
503no_nodeNo sending server is available right now. Retry later.

Send a message: POST /v1/send

FieldTypeDescription
fromstringRequired. One address, such as noreply@example.com or Example <noreply@example.com>. The domain must be verified for the stream.
tostring or arrayRequired. One address, a comma-separated string, or an array. Array items can also be objects {"email": "...", "name": "..."}.
cc, bccstring or arrayOptional. Same format as to. Bcc addresses don't appear in the headers.
reply_tostring or arrayOptional. Reply-To address.
subjectstringRequired. One line, up to 998 characters.
html, textstringAt least one is required. Send both for the best results.
headersobjectOptional extra headers, such as {"X-Campaign": "spring"}. Standard headers (From, To, Subject, Reply-To, Message-ID, List-Unsubscribe and so on) can't be set here.
attachmentsarrayOptional. Items {"filename": "invoice.pdf", "content_base64": "...", "type": "application/pdf"}. At most 10 files and 10 MB in total.
categorystringOptional. Letters, digits, ., _, -, up to 64 characters.
tagsarray of stringsOptional. Up to 10 tags for your own filtering, each up to 64 characters.
track_opens, track_clicksbooleanOptional. Turn open tracking (a tracking pixel) and click tracking (links rewritten through t.tm.yeapcloud.net) on or off for this message. HTML only. If omitted, the stream's default applies. Both are off by default.
variablesobjectOptional. Replaces {{name}} placeholders in subject, html and text. Values are HTML-escaped in html. Unknown placeholders are left as they are.

A single call can have up to 50 recipients in total across to, cc and bcc. Each recipient gets a separate copy with its own message ID. To send different content to many people, use the batch endpoint.

Example:

bash
curl -X POST https://api.tm.yeapcloud.net/v1/send \
  -H "Authorization: Bearer ytm_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "Example <noreply@example.com>",
    "to": ["alice@example.org"],
    "subject": "Your order {{order_id}} has shipped",
    "html": "<p>Hi {{name}}, your order is on its way.</p>",
    "text": "Hi {{name}}, your order is on its way.",
    "category": "shipping",
    "tags": ["order"],
    "variables": {"name": "Alice", "order_id": "A-1024"}
  }'

Response:

json
{
  "message_ids": ["01JAQ3Z9V8K4M2N6P7R5S1T0XW"],
  "rejected": []
}

message_ids lists the accepted copies. rejected lists recipients that weren't sent, each with a rcpt and a reason. A call can succeed with some or even all recipients rejected, so always check rejected:

reasonMeaning
invalid_recipientThe address is invalid
suppressed:<scope>:<reason>The address is on a suppression list, for example suppressed:global:hard_bounce
sandbox_recipient_not_verifiedThe stream is in the sandbox and the recipient isn't allowed
rate_limited_hourly, rate_limited_sandbox_or_hourlyThe hourly limit (or the sandbox daily limit) is used up
monthly_quota_exceededThe monthly volume is used up and overage isn't allowed
enqueue_failedThe message couldn't be queued. It isn't counted against your quota. Retry.

Python example with requests:

python
import requests

resp = requests.post(
    "https://api.tm.yeapcloud.net/v1/send",
    headers={"Authorization": "Bearer ytm_your_api_key"},
    json={
        "from": "Example <noreply@example.com>",
        "to": "alice@example.org",
        "subject": "Welcome",
        "html": "<p>Welcome aboard!</p>",
        "text": "Welcome aboard!",
        "category": "welcome",
    },
    timeout=30,
)
resp.raise_for_status()
result = resp.json()
print(result["message_ids"], result["rejected"])

Send in bulk: POST /v1/batch

Send up to 500 messages in one request. Each item has the same fields as /v1/send.

json
{
  "messages": [
    {"from": "noreply@example.com", "to": "a@example.org", "subject": "Hi A", "text": "Hello A"},
    {"from": "noreply@example.com", "to": "b@example.org", "subject": "Hi B", "text": "Hello B"}
  ]
}

The response has one result per message, in the same order:

json
{
  "results": [
    {"index": 0, "message_ids": ["01JAQ..."], "rejected": []},
    {"index": 1, "error": {"code": "invalid_request", "message": "subject is required (single line)"}, "rejected": []}
  ]
}

Look up messages: GET /v1/messages

Query parameterDescription
statusFilter by status: queued, delivered, deferred, bounced, complained, rejected or failed
toFilter by recipient address
sinceOnly messages created at or after this time, such as 2026-10-01T00:00:00Z
limitResults per page, 1–100 (default 50)
beforeFor the next page, pass the next_before value from the previous response

The response is {"data": [...], "next_before": "..."}. next_before is null on the last page. Messages are kept for 30 days.

GET /v1/messages/{id} returns one message and its events:

json
{
  "id": "01JAQ3Z9V8K4M2N6P7R5S1T0XW",
  "stream_id": 12,
  "source": "api",
  "to": "alice@example.org",
  "from": "Example <noreply@example.com>",
  "subject": "Welcome",
  "category": "welcome",
  "tags": [],
  "status": "delivered",
  "smtp_code": 250,
  "smtp_response": "250 2.0.0 OK",
  "opened_at": null,
  "clicked_at": null,
  "unsubscribed_at": null,
  "created_at": "2026-10-20 10:15:02",
  "updated_at": "2026-10-20 10:15:04",
  "events": [
    {"type": "delivered", "at": "2026-10-20 10:15:04", "rcpt": "alice@example.org", "smtp_code": 250, "mx": "mx.example.org"}
  ]
}

Suppressions: /v1/suppressions

These endpoints manage the suppression list of the API key's stream.

RequestBody or queryResponse
GET /v1/suppressionsQuery email (optional), limit (1–500, default 100), offset{"data": [{"id", "scope", "email", "reason", "category", "note", "created_at"}]}
POST /v1/suppressions{"email": "..."} or {"emails": [...]} (up to 1,000), optional category{"added": 2, "invalid": []}
DELETE /v1/suppressions{"email": "..."}, optional category (body or query){"removed": 1}

Addresses added through the API have the reason manual. Account-wide and global suppressions can't be changed with the API. See Suppressions and unsubscribe.

Adakah halaman ini membantu?