Authentication
Every call to the REST API at https://api.hatang.dev is authenticated with an API key sent as a Bearer token. This page shows how to create keys, which scopes to give them, how to restrict them, and what the API answers when a key is missing, wrong or not allowed.
Before you start
You need to be an owner or admin of the business, or a member with Developer access, to create and revoke keys (Team and roles).
Create an API key
- In the dashboard, open Settings → API Keys. Email users also find the same page under Email → API Keys.
- Click Create key and give the key a name that says where it runs, such as
production-weborerp-sync. - Under What this key can do, tick the scopes it needs. Only the modules that are turned on for the business are listed.
- Optionally restrict the key to one Sending domain or to Browser origins (see Restricted keys).
- Click Create key and copy the key. It starts with
ht_and is shown only once: Ha Tang stores only a hash of it.
The keys table shows each key's prefix, what it can do and when it was Last used.
Send the key
Put the key in the Authorization header of every request:
Authorization: Bearer ht_123...
- cURL
- Node.js
- Python
- PHP
curl "https://api.hatang.dev/emails?limit=1" \
-H "Authorization: Bearer $HATANG_API_KEY"
import { Hatang } from "@hatang/sdk";
// The SDK sends the Authorization header for you.
const hatang = new Hatang(process.env.HATANG_API_KEY, { baseUrl: "https://api.hatang.dev" });
const { data, error } = await hatang.emails.list({ limit: 1 });
import os
from hatang import Hatang
# The SDK sends the Authorization header for you.
hatang = Hatang(os.environ["HATANG_API_KEY"], base_url="https://api.hatang.dev")
emails = hatang.emails.list(limit=1)
$client = new GuzzleHttp\Client([
'base_uri' => 'https://api.hatang.dev',
'headers' => ['Authorization' => 'Bearer ' . getenv('HATANG_API_KEY')],
]);
$res = $client->get('/emails', ['query' => ['limit' => 1]]);
The same key also works as the SMTP password for smtp.hatang.dev:5870 (STARTTLS), with the username hatang.
An API key acts on behalf of your whole business. Keep it in an environment variable or a secret manager on your server. Never put it in browser JavaScript, a mobile app or a Git repository. Browsers and apps use the chat widget and the mobile SDKs, which have their own public site id and app keys.
Scopes
A scope has the form <product>:<verb>, <product>:* or *.
| Module | Scopes | What they allow |
|---|---|---|
email:send | Send email: POST /emails, POST /emails/batch and the SMTP relay | |
email:read | List and fetch emails, metrics and received mail | |
email:write | Manage domains, templates, contacts, audiences, segments, topics, broadcasts and automations | |
| Support | chat:read, chat:write | Conversations, messages, visitors, tickets and widget sites; write also replies, assigns and resolves |
| Payments | payments:read, payments:write | Payments, transactions, bank connections and e-invoices; write also creates payments and issues invoices |
| Products | products:read, products:write | Products, categories and orders, including an order's payment link |
| CRM | crm:read, crm:write | Customers, custom fields, merging, CSV export and ZNS |
| Everything | * | Full access to every module |
How scopes combine:
writeandsendboth includeread. A key withemail:sendcan read the emails it sent.<product>:*covers every verb of that product, and*covers every product.- Products and CRM grew out of Support: a key with
chat:read,chat:writeorchat:*keeps working on the Products and CRM endpoints. New keys can useproducts:*andcrm:*to narrow access. - A few endpoints belong to the whole business rather than a module. Managing webhook endpoints, reading API Logs and reading Usage need any
writescope; managing API keys through the API needsemail:write.
Reading received mail (/emails/receiving) needs email:read or email:write granted on the key. The read access that email:send implies is not enough there.
The dashboard offers the scopes of Email, Support and Payments. To create a key with products:*, crm:* or *, call POST /api-keys with a key that has email:write:
curl -X POST https://api.hatang.dev/api-keys \
-H "Authorization: Bearer $HATANG_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "erp-sync", "scopes": ["products:*", "crm:read"] }'
{ "id": "key_123...", "token": "ht_123..." }
A key needs the right scope and its module must be turned on. A call to a module that is off answers 403 product_disabled even with the right scope (Businesses and modules).
Restricted keys
Two optional restrictions limit the damage if a key leaks.
| Restriction | Where to set it | Effect |
|---|---|---|
| Sending domain | Dashboard, or domain_id on POST /api-keys | The key can only send from that verified domain. Sending from another domain fails with 401 restricted_api_key. Only applies to keys with an Email scope. |
| Browser origins | Dashboard: Where will this key run? → In the browser, then one origin per line, such as https://shop.vn | Requests must carry an Origin header from the list. Anything else, including server requests without an Origin header, fails with 401 restricted_api_key. |
Choose On your server (the default) for keys used from your backend; such a key has no origin lock. Choose In the browser only for a key that a browser loads. The error message says when a browser-only key was used without an Origin header.
Test keys and the Payments environment
Only the Payments module has environments. Every business has a development environment, with a simulated bank and the e-invoice provider's sandbox, and a production environment with real money. Payments data never crosses between them. Email, Support, Products and CRM have no environments.
Pick the environment per request with the Hatang-Environment header:
curl https://api.hatang.dev/payments \
-H "Authorization: Bearer $HATANG_API_KEY" \
-H "Hatang-Environment: development"
| Key | No header | Hatang-Environment: development | Hatang-Environment: production |
|---|---|---|---|
ht_... | production | development | production |
ht_test_... | development | development | 403 environment_mismatch |
- Every response repeats the environment it used in the
Hatang-Environmentheader. - Any other header value fails with
422 validation_error. - In the Node SDK, pass
environment: "development"to theHatangconstructor, or setHATANG_ENVIRONMENT. - Keys that start with
ht_test_are development keys from earlier versions of Ha Tang. They can never reach production data. New keys start withht_and use the header.
See Payments environments for what differs between the two.
Rotate and revoke keys
Rotate a key when someone who knew it leaves, when it may have leaked, or on a regular schedule:
- Create a new key with the same scopes and restrictions.
- Deploy it everywhere the old key was used.
- Watch the old key's Last used column until it stops changing.
- Click Revoke on the old key, or call
DELETE /api-keys/{id}.
A revoked key stops working within a minute. Revoking cannot be undone; create a new key instead.
Errors
| Status | name | When |
|---|---|---|
| 401 | missing_api_key | No Authorization: Bearer header |
| 403 | invalid_api_key | The key is malformed, unknown or revoked |
| 401 | restricted_api_key | The key lacks the scope, is used from an origin it does not allow, or sends from a domain it is not locked to |
| 403 | product_disabled | The module behind the endpoint is turned off for the business |
| 403 | environment_mismatch | A ht_test_ key asked for production, or a Payments action does not exist in this environment |
| 422 | validation_error | Hatang-Environment is not development or production |
{
"statusCode": 401,
"name": "restricted_api_key",
"message": "This API key does not have the payments:write scope"
}
Branch on name, not on message. The message is for people and may change. None of these errors go away by retrying: fix the key, the scope or the module first. See Errors for the full picture.