API conventions
The conventions HyoNova APIs follow — base URL, JSON errors, request IDs, HTTP status codes, cursor pagination, idempotency keys and rate limits — as the Mail API applies them.
On this page
HyoNova's APIs share a small set of conventions for errors, pagination, retries and limits. The Mail API follows them today. A general public API is planned and will follow the same conventions.
Requests and responses#
- Requests and responses use JSON (
Content-Type: application/json). - Every response carries an
X-Request-Idheader. Include it when you contact HyoNova about a problem. - Responses to API requests are not cached (
Cache-Control: no-store).
Errors#
Every error has the same shape, whatever the endpoint:
{
"error": {
"code": "SENDER_NOT_VERIFIED",
"message": "Add and verify a sender before sending."
}
}codeis a stable, machine-readable string. Branch on it, not on the message.messageis a human-readable explanation. It can change and is not meant for parsing.
Unexpected server errors return a generic message (INTERNAL_ERROR). Details are logged on HyoNova's side under the request ID, never returned in the response.
HTTP status codes#
| Status | Meaning | Examples of code |
|---|---|---|
200 | Success | — |
202 | Accepted for processing, such as an email queued for delivery | — |
400 | The request is invalid | VALIDATION_ERROR |
401 | No credential, or the credential is no longer valid | AUTHENTICATION_REQUIRED, API_KEY_INVALID, API_KEY_EXPIRED |
402 | Not enough credits for this request | INSUFFICIENT_CREDITS |
403 | Authenticated, but not allowed to do this | FORBIDDEN, FEATURE_NOT_ENTITLED |
404 | The resource does not exist or is not visible to you | NOT_FOUND |
409 | Conflict with the current state | SENDER_NOT_VERIFIED, IDEMPOTENCY_CONFLICT |
413 | The request body is too large | — |
429 | Too many requests | — |
500 | Unexpected error on HyoNova's side | INTERNAL_ERROR |
Records that belong to another company are never visible, whatever the request or the key.
Pagination#
List endpoints — GET /mail/v1/messages and GET /mail/v1/contacts — use an opaque cursor.
- Pass
limitto choose the page size. The default is 25 and the maximum is 100. - The response has
itemsandnextCursor. WhennextCursorisnull, there are no more pages. - To get the next page, pass the
nextCursorvalue back ascursor. Do not build or change cursors yourself.
{
"items": [{ "id": "…" }, { "id": "…" }],
"nextCursor": "b3f1…"
}Idempotency#
POST endpoints accept an Idempotency-Key header. Send a new unique value for each logical operation, and reuse the same value when you retry that operation after a timeout or a network error.
- The first answer under a key is kept for 24 hours for that API key. A retry with the same key and body gets the same answer again, with
Idempotent-Replayed: true. - The same key with a different body answers
409 IDEMPOTENCY_CONFLICT. - On
POST /mail/v1/send, the key also names the message for your whole company: a later call with the same key returns the original message with"replayed": trueand sends nothing new. Use a key that names the message, such asorder-1001-confirmation, up to 160 characters.
Rate limits#
The Mail API accepts up to 600 requests per minute per API key (300 per minute for POST /mail/v1/conversions). Every response carries x-ratelimit-limit, x-ratelimit-remaining and x-ratelimit-reset. When you exceed the limit you receive the HTTP status 429 with a retry-after header in seconds. Detect it by the status code, wait, and use increasing delays between attempts.
Example: handling errors#
async function sendWithRetry(body: unknown, idempotencyKey: string) {
for (let attempt = 0; attempt < 5; attempt++) {
const response = await fetch('https://api.hyonova.com/mail/v1/send', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.HYO_MAIL_API_KEY}`, // YOUR_API_KEY
'Content-Type': 'application/json',
'Idempotency-Key': idempotencyKey, // the same key on every retry
},
body: JSON.stringify(body),
});
if (response.status === 202) return response.json();
if (response.status === 429 || response.status >= 500) {
const wait = Number(response.headers.get('retry-after')) || 2 ** attempt;
await new Promise((r) => setTimeout(r, wait * 1000)); // back off, then retry
continue;
}
const { error } = await response.json();
const requestId = response.headers.get('X-Request-Id');
throw new Error(`${error.code} (${requestId}): ${error.message}`); // do not retry
}
throw new Error('Gave up after 5 attempts');
}curl -i -X POST "https://api.hyonova.com/mail/v1/send" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1001-confirmation" \
-d '{"to": "name@example.com", "subject": "Your order", "html": "<p>Thank you.</p>"}'
# -i prints the status line, X-Request-Id and the x-ratelimit-* headersStatus#
| Feature | Status | Notes |
|---|---|---|
| Transactional email API | AvailableWorks today for workspaces whose plan includes it. | Send email from your own systems with API keys, a message log and signed webhooks. Part of Hyo Mail. |
| Signed webhooks | AvailableWorks today for workspaces whose plan includes it. | Receive signed Hyo Mail delivery, engagement, contact and store events at your HTTPS endpoint, with automatic and manual retries. |
| SMTP relay for Hyo Mail | PlannedOn the HyoNova roadmap. Not built yet and has no release date. | Send through Hyo Mail over SMTP. |
| Public HyoNova API | PlannedOn the HyoNova roadmap. Not built yet and has no release date. | A supported, documented API for your company’s people, work and customer data. |
| MCP server | PlannedOn the HyoNova roadmap. Not built yet and has no release date. | Let AI assistants work with your HyoNova data through the Model Context Protocol, with your permission. |
Was this page helpful?
Thanks. Page ratings are not collected yet, so nothing was sent. If something is missing or wrong, see the Get help page.