Quickstart: send your first email
This guide takes you from a new account to a delivered email in about ten minutes. You create an API key, verify the domain you send from, send a message through the REST API and then find it in the dashboard.
Before you start
- A Ha Tang account at app.hatang.dev with the Email module turned on (Businesses and modules).
- Access to the DNS settings of a domain you own, such as
shop.vn. - To create keys you need to be an owner or admin, or a member with the Developer access (Team and roles).
1. Create an API key
- In the dashboard, open Settings → API Keys (the same page is also under Email → API Keys).
- Click Create key, give it a name such as
production, and tick Send under Email. That grants theemail:sendscope, which is all you need to send. - Optionally pick a Sending domain to lock the key to one domain.
- Copy the key. It starts with
ht_and is shown only once.
Store it as an environment variable on your server:
export HATANG_API_KEY="ht_123..."
API keys belong on your server. Never put one in browser JavaScript or a mobile app bundle. See Authentication for scopes and restricted keys.
2. Verify a sending domain
Ha Tang only sends from domains you have proved you own.
- Open Email → Domains and add your domain, for example
shop.vn. - Add the DNS records Ha Tang shows you at your DNS provider.
- Wait for the domain to show as Verified.
The Domains guide explains each record, how long verification takes and how to fix a domain that stays pending.
If you send from a domain that is not verified, the API answers 403 with name: "validation_error" and a message such as The shop.vn domain is not verified. Please, add and verify your domain first.
3. Send an email
Send a POST request to https://api.hatang.dev/emails. The from address must be on your verified domain. to takes one address or a list.
- 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": "Shop <[email protected]>",
"to": ["[email protected]"],
"subject": "Your order is confirmed",
"html": "<p>Thanks for your order!</p>"
}'
npm install @hatang/sdk
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) {
console.error(error.name, error.message);
} else {
console.log("Sent", data.id);
}
from hatang import Hatang, HatangError # pip install hatang
hatang = Hatang() # reads HATANG_API_KEY
try:
email = hatang.emails.send({
"subject": "Your order is confirmed",
"html": "<p>Thanks for your order!</p>",
})
print("Sent", email["id"])
except HatangError as err:
print(err.name, err.message)
<?php
require 'vendor/autoload.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>',
],
'http_errors' => false,
]);
$body = json_decode((string) $res->getBody(), true);
if ($res->getStatusCode() === 200) {
echo 'Sent ' . $body['id'];
} else {
echo $body['name'] . ': ' . $body['message'];
}
A successful call answers 200 with the id of the new email:
{ "id": "01a0b904-2978-73e8-b352-4a26fdca0ab5" }
The email is queued and delivered in the background. You need html, text or a published template; to, cc and bcc together take at most 50 addresses.
Add an Idempotency-Key header (for example your order id) so a retried request never sends the same email twice. See Idempotency.
Pass baseUrl: "https://api.hatang.dev" (or set the HATANG_BASE_URL environment variable) when you create the client, as in the example above.
4. Check it in the dashboard
- Email → Emails lists every message with its status: queued, sent, delivered, bounced and so on. Open one to see each recipient and the delivery events.
- Email → Logs lists every API request your keys made, with the status code, duration and request and response bodies. A failed call shows up here with its error.
You can read the same status from the API:
curl https://api.hatang.dev/emails/01a0b904-2978-73e8-b352-4a26fdca0ab5 \
-H "Authorization: Bearer $HATANG_API_KEY"
The response is shortened here:
{
"object": "email",
"id": "01a0b904-2978-73e8-b352-4a26fdca0ab5",
"subject": "Your order is confirmed",
"last_event": "delivered",
"created_at": "2026-10-23T09:34:12.952Z"
}
What can go wrong
| Status | name | Cause | Fix |
|---|---|---|---|
| 401 | missing_api_key | No Authorization header | Send Authorization: Bearer ht_... |
| 403 | invalid_api_key | The key is wrong or revoked | Copy the key again or create a new one |
| 401 | restricted_api_key | The key is locked to a different sending domain | Use a key for this domain, or one for all domains |
| 403 | validation_error | The from domain is not verified | Finish domain verification |
| 422 | validation_error | A field is missing or invalid; message names it | Fix the request body |
| 403 | product_disabled | The Email module is turned off | Ask an owner or admin to turn it on |
| 429 | rate_limit_exceeded | More than 10 email requests per second | Back off and retry (Rate limits) |
| 429 | daily_quota_exceeded | The business reached its daily email quota | Wait for midnight UTC or raise the quota in Email → Email settings |
Next steps
- Get delivery events with webhooks
- Email overview: templates, broadcasts, audiences and inbound mail
- Metrics and logs
- Authentication