Ha Tang Email
Ha Tang Email sends transactional and marketing email from your own domain. It speaks the same API as Resend, so existing Resend code works after you change the base URL and the API key, and it delivers through Amazon SES. This page shows how the pieces fit together and walks you through your first email.
What you get
| Feature | What it does | Guide |
|---|---|---|
| Sending domains | DKIM, SPF and return path for each domain, one-click DNS setup at supported DNS hosts | Domains |
| REST API | POST /emails and POST /emails/batch, scheduling, idempotency, tags, attachments | Send email |
| SMTP relay | smtp.hatang.dev:5870 with STARTTLS, for frameworks and apps that only speak SMTP | SMTP relay |
| Templates | Reusable emails with variables, sent by id or alias | Templates |
| Tracking | Open and click tracking, optionally on your own subdomain | Open and click tracking |
| Receiving | Mail sent to your domain becomes an email.received webhook | Receive email |
| Audiences | Contacts, segments, topics and custom properties | Contacts, segments and topics |
| Broadcasts | Marketing email to a segment, now or on a schedule, with a hosted preferences page | Broadcasts |
| Automations | Per-contact sequences started by events from your app | Automations |
| Suppressions | Hard bounces, complaints and unsubscribes stop further sends | Suppressions and bounces |
| Metrics and logs | Delivery, open, click, bounce and complaint rates, plus a log of every API call | Metrics and logs |
How a message travels
- Your app calls
POST /emailsor hands a message to the SMTP relay. Both go through the same checks. - Ha Tang checks that the
Fromdomain is verified and belongs to your business, skips suppressed recipients and reserves one email from your daily quota. It answers with the emailidright away. - A worker signs the message with your domain's DKIM key, sets the bounce address on your return-path subdomain (
send.by default) and hands it to Amazon SES. - Amazon SES reports deliveries, bounces and complaints back to Ha Tang. Ha Tang updates the email's status, adds hard bounces and complaints to your suppression list and sends you webhooks.
Before you start
- A Ha Tang business with the Email module turned on. See Businesses and modules.
- A domain you can add DNS records to.
- An API key. See Authentication.
Send your first email
1. Add and verify a domain
In the dashboard, open Email → Domains, add your domain (for example example.com or a subdomain such as updates.example.com), publish the DNS records it shows and click Verify DNS records. The domain must reach the Verified status before you can send from it. The details are in Domains.
2. Create an API key
Create a key with the email:send scope. Keep it on your server. Never put it in a browser or a mobile app.
3. Send
- cURL
- Node.js
- Python
- PHP
curl -X POST https://api.hatang.dev/emails \
-H "Authorization: Bearer $HATANG_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "Acme <[email protected]>",
"to": ["[email protected]"],
"subject": "Your order is confirmed",
"html": "<p>Thanks for your order!</p>"
}'
import { Hatang } from "@hatang/sdk";
const hatang = new Hatang(process.env.HATANG_API_KEY, { baseUrl: "https://api.hatang.dev" });
const { data, error } = await hatang.emails.send({
subject: "Your order is confirmed",
html: "<p>Thanks for your order!</p>",
});
if (error) throw new Error(`${error.name}: ${error.message}`);
console.log(data.id);
from hatang import Hatang # pip install hatang
hatang = Hatang() # reads HATANG_API_KEY
email = hatang.emails.send({
"subject": "Your order is confirmed",
"html": "<p>Thanks for your order!</p>",
})
print(email["id"])
<?php
$client = new GuzzleHttp\Client(['base_uri' => 'https://api.hatang.dev']);
$res = $client->post('/emails', [
'headers' => ['Authorization' => 'Bearer ' . getenv('HATANG_API_KEY')],
'json' => [
'subject' => 'Your order is confirmed',
'html' => '<p>Thanks for your order!</p>',
],
]);
echo json_decode($res->getBody(), true)['id'];
The response holds the email's id:
{ "id": "0199e6c1-7a2b-7f00-9c1e-3f6a2b1d9e10" }
The Node.js SDK returns { data, error } and never throws. Always pass baseUrl: "https://api.hatang.dev" (or set HATANG_BASE_URL), because the SDK's built-in default points at a local development server.
4. Follow the email
- In the dashboard, Email → Emails lists every email with its status, recipients and events.
GET /emails/{id}returns the email and itsstatus;GET /emails/{id}/eventsreturns each event and each recipient's state.- For real-time updates, subscribe a webhook to
email.delivered,email.bouncedand the other events. See Webhooks.
Dashboard map
| Sidebar item | What you do there |
|---|---|
| Emails | Every email sent through the API, the SMTP relay or broadcasts. The Receiving tab lists mail received on your domains. |
| Broadcasts | Write, schedule and send marketing email to a segment. |
| Templates | Edit, test and publish templates. |
| Audiences | Contacts, segments, topics, properties and the suppression list. |
| Automations | Event-driven sequences and their runs. |
| Metrics | Volume and rates per day. |
| Domains | Sending and receiving domains, DNS records, tracking. |
| Logs | Every API call made with your keys. |
| API Keys | Create and revoke API keys. |
| Webhooks | Endpoints that receive email events. |
| Email settings | SMTP connection details, daily quota, the emergency pause and the unsubscribe page. |
API key scopes
Grant each key only the scopes the system using it needs:
| Endpoints | Required scope |
|---|---|
POST /emails, POST /emails/batch, the SMTP relay | email:send |
PATCH /emails/{id}, POST /emails/{id}/cancel | email:send |
GET /emails, GET /emails/{id}, GET /emails/{id}/events | email:read (or email:send / email:write, which include it) |
| Domains, templates, contacts, segments, topics, broadcasts, events, automations | email:write |
GET /emails/metrics | email:read (or email:send / email:write, which include it) |
GET /emails/receiving/* | email:read or email:write, granted directly. email:send alone cannot read received mail. |
GET /logs, GET /usage | Any write scope |
A key can also be limited to one sending domain. That key gets 401 restricted_api_key when it sends from another domain.
Limits at a glance
| Limit | Value |
|---|---|
| REST API rate | 10 requests per second per business, shared by the email endpoints |
| Recipients per email | 50 across to, cc and bcc |
| Emails per batch call | 100 |
| Email size | 40 MB, body and attachments together |
| Scheduling | Up to 30 days ahead |
| Daily quota | Set per business, resets at midnight UTC. See Email settings or GET /usage. |
The full list is in Limits and every error name is in Errors.
Next steps
- Domains: set up DKIM, SPF and DMARC.
- Send email: every field of
POST /emails. - SMTP relay: connect WordPress, Laravel, Django and others.
- Migrate from Resend: what changes when you switch.