> ## Documentation Index
> Fetch the complete documentation index at: https://docs.hiveku.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Migrate from Resend

> Move an app from Resend to Hiveku by changing two settings

Hiveku has a Resend-compatible API at `https://app.hiveku.com/api/v1/email/resend`. An app built on Resend's SDK or REST API moves to Hiveku by changing its API key and base URL. The requests and answers are Resend's, and every email runs the same checks as Hiveku's own [REST API](https://docs.hiveku.com/email/sending.md).

## Before you switch

**1. Verify your domain in Hiveku**

Add the domain you send from and publish its records. Keep Resend's DNS records until the switch is done: Hiveku's records sit beside them. See [Moving from another provider](https://docs.hiveku.com/email/domains.md#moving-from-another-provider).

**2. Create a key**

Create a **Production** key in **Settings > API Keys**. Add **Read Emails** only if your app calls `emails.get`. See [API keys](https://docs.hiveku.com/email/api-keys.md).

**3. Recreate your webhooks**

Add your endpoint as a Hiveku [webhook](https://docs.hiveku.com/email/webhooks.md). The event names match Resend's, Hiveku sends the same signature headers, and an event's `data` carries the same `email_id`, `from`, `to`, `subject` and `tags`. Give your handler Hiveku's signing secret, and check any other field it reads.

**4. Bring over your suppressions**

Hiveku suppresses an address on its own after a hard bounce or a spam complaint, but it does not know who bounced, complained or unsubscribed at Resend. Add those addresses before you switch. See [Suppressions](#suppressions).

## Switch

Set two environment variables:

```bash theme={null}
RESEND_API_KEY=hk_live_...
RESEND_BASE_URL=https://app.hiveku.com/api/v1/email/resend
```

Your code does not change:

```ts theme={null}
import { Resend } from "resend";

const resend = new Resend(process.env.RESEND_API_KEY);

const { data, error } = await resend.emails.send({
  from: "Acme <noreply@mail.acme.com>",
  to: ["customer@example.com"],
  subject: "Your order shipped",
  html: "<p>On the way.</p>",
});
```

> [!NOTE]
>
> The Resend SDK reads `RESEND_BASE_URL` once, when it is first imported. Set it in the environment the app starts with, not in code after the import. Hiveku tests this with the `resend` package, version 6.

If you call Resend's REST API yourself, send the same requests to the Hiveku base URL:

```bash theme={null}
curl -X POST https://app.hiveku.com/api/v1/email/resend/emails \
  -H "Authorization: Bearer $RESEND_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "Acme <noreply@mail.acme.com>",
    "to": ["customer@example.com"],
    "subject": "Your order shipped",
    "html": "<p>On the way.</p>"
  }'
```

Try it with a test key (`hk_test_...`) first. The answer adds a warning that nothing was delivered:

```json theme={null}
{
  "id": "5f0c7a2e-8d1b-4c3e-9a4f-2b6d8e1f0a37",
  "warnings": ["Email simulated, nothing delivered: this is a test key, and only production keys send real email."]
}
```

## What works

| Resend call | Endpoint | Notes |
| - | - | - |
| `emails.send` | `POST /emails` | Answers `{ "id": "..." }`. |
| `batch.send` | `POST /emails/batch` | Up to 100 emails, without attachments. With `strict` validation, the default, one email that cannot go refuses the whole batch, and the answer names it. With `permissive`, the others go, and `errors` lists the refused ones by `index`. |
| `emails.get` | `GET /emails/{id}` | Needs **Read Emails**. A key reads only the emails it sent. A test key's emails read back with `last_event: "delivered"`, so code that waits for delivery runs the same in testing. |
| `emails.cancel`, `emails.update` | `POST /emails/{id}/cancel`, `PATCH /emails/{id}` | Only for scheduled emails. Scheduled sending is not available yet, so there is nothing to cancel or move. |

The fields of an email:

* `from`, `to`, `cc`, `bcc`, at most 50 recipients in all
* `subject`, `html`, `text`, and `react`, which the SDK turns into `html` before it calls Hiveku
* `replyTo`
* `headers`, of which only some are delivered: see [Headers](https://docs.hiveku.com/email/sending.md#headers)
* `tags`, as Resend's list or as an object: see [Tags](https://docs.hiveku.com/email/sending.md#tags)
* `attachments`, with `content` (base64 text or a Buffer), `filename`, `contentType` and `contentId`, 25 MB in all
* idempotency keys (`idempotencyKey` in the SDK, the `Idempotency-Key` header), kept 24 hours

A Buffer travels as JSON at about 3.6 times its size, and a request may be at most 50 MB. For a large file, send base64 instead: `content: buffer.toString("base64")`.

## What is refused

Hiveku refuses what it cannot honor, rather than sending something else:

| You send | Answer |
| - | - |
| An attachment with `path`, a URL to fetch | `422 invalid_attachment`. Send the file itself in `content`. |
| `template`, a Resend-hosted template | `422 validation_error`. Send `html` or `text`, or a Hiveku template through [send-template](https://docs.hiveku.com/email/sending.md#send-with-a-template). |
| `topicId` | `422 validation_error`. Hiveku cannot check who opted out of a topic. Add those people to your [suppressions](#suppressions), then send without it. |
| `scheduledAt` in the future | `400 validation_error`. Scheduled sending is not available yet. |
| Attachments in a batch | `400 validation_error`. Send those emails one at a time. |
| A list of emails, `GET /emails` | `405 method_not_allowed`. Use **Settings > Email Service > Email Logs**. |
| Domains, API keys, contacts, audiences, broadcasts and the rest | `404 not_found`. Manage domains and keys in the dashboard. |

In a batch, these refuse the whole batch under `strict` validation. Under `permissive`, the refused email is listed in `errors` and the others go.

Resend's contacts, audiences and broadcasts have no compatible API here. Hiveku's [marketing email](https://docs.hiveku.com/email-marketing/overview.md) has its own audiences and campaigns.

## Differences to know

* **Warnings.** An answer may add `warnings`: what Hiveku left out, such as a header it does not deliver or a reserved tag, or what it did not deliver. The SDK hands the field back in `data`.
* **Suppressed recipients.** An email whose every recipient is suppressed is not sent, but it is recorded and answered with its id and a warning. It reads back with `last_event: "suppressed"`.
* **Headers.** Only some headers are delivered. See [Headers](https://docs.hiveku.com/email/sending.md#headers).
* **Reserved tags.** Tags named `channel` or `campaign_id`, or starting with `hiveku_`, are dropped with a warning.
* **Limits.** Rates and monthly emails follow your Hiveku plan. A `429` carries `Retry-After`. See [Limits](https://docs.hiveku.com/email/api-keys.md#limits).
* **Old keys.** A Resend key (`re_...`) left in place is refused with `401 invalid_api_key`, and the message says to use a Hiveku key.

## Errors

Errors come in Resend's shape, `{ "statusCode": 422, "name": "missing_required_field", "message": "Missing required field: subject" }`. Resend clients branch on `name`:

| Status | `name` | When |
| - | - | - |
| `401` | `missing_api_key` | There is no `Authorization` header. |
| `401` | `invalid_api_key` | The key is wrong, paused or deleted, or it is a Resend key. |
| `403` | `invalid_access` | The key may not send. |
| `403` | `restricted_api_key` | A key without **Read Emails** called `emails.get`. |
| `422` | `missing_required_field` | `from`, `to`, `subject`, or both `html` and `text` are missing, or a batch is empty. |
| `400`, `402`, `403`, `413`, `422` | `validation_error` | Any other refusal: the domain is not verified (`403`), billing blocks sending (`402`), a field Hiveku cannot honor (`422`), and so on. `message` says which. |
| `400` | `invalid_idempotency_key` | The idempotency key is empty or over 256 characters. |
| `409` | `invalid_idempotent_request` | The key was used with a different request, or the first request's result is unknown. |
| `409` | `concurrent_idempotent_requests` | The first request with this key is still being sent. |
| `413`, `422` | `invalid_attachment` | Attachments over 25 MB, or an attachment with `path`. |
| `429` | `rate_limit_exceeded` | The key's rate. |
| `429` | `monthly_quota_exceeded` | The month's emails are used up. |
| `429` | `daily_quota_exceeded` | The [daily volume check](https://docs.hiveku.com/email/api-keys.md#daily-volume-check) holds API mail until 00:00 UTC. |
| `404` | `not_found` | No such email for this key, or an endpoint Hiveku does not serve. |
| `405` | `method_not_allowed` | Listing emails. |
| `500` | `application_error` | The email could not be sent, or something failed on Hiveku's side. When the email provider gave no answer, the message says the email may have gone: look for it in **Email Logs** before sending again, because a retry with the same idempotency key answers `409`. |

## Suppressions

Add each address that bounced, complained or unsubscribed at Resend with `POST /api/v1/email/suppressions`:

```bash theme={null}
curl -X POST https://app.hiveku.com/api/v1/email/suppressions \
  -H "Authorization: Bearer $HIVEKU_SUPPRESSIONS_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "email": "bounced@example.com", "reason": "Bounced at Resend" }'
```

It takes one address per request, answers `201`, and answers `409 already_exists` for an address already on the list. The key needs the `email:suppressions` permission. The dashboard's key form does not offer it, but your AI assistant can create such a key.

## After the switch

* Watch **Settings > Email Service > Email Logs**, in the **App sends (API and SMTP)** view, as your app's email moves over.
* Test with a test key, not a trickle of real sends. Simulated sends are not counted, so they do not set a small usual day for the [daily volume check](https://docs.hiveku.com/email/api-keys.md#daily-volume-check). If your account already sends API mail on most days, moving a large app over in one go can pass 5 times that usual day and hold API mail until 00:00 UTC.
* Once nothing sends through Resend, remove Resend's DNS records and its `include:` from your SPF record.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.