結論

Resend(リセンド)は、アプリから会員登録の確認やパスワード再設定などのメールを送るための、開発者向けのメール送信APIです。APIキーを付けて POST /emails に JSON を送るか、SMTP でつなぐだけで送れます。無料プランは月3,000通・1日100通まで(2026年10月29日時点)です。

この記事でわかること

  • Node.js・Python・curl の送信リクエストとエラー応答の中身
  • React Email の出力、Webhook の署名検証、MailDev で受けたメールの実物
  • 料金と、SendGrid・Amazon SES・Postmark・Mailgun との判断表

2026年10月29日更新|resend 6.35.0(Node.js)・resend 2.50.1(Python)・react-email 6.11.6 を 127.0.0.1 のモックで実行確認

Resend は、メールを「どう届けるか」を引き受けるサービスです。アプリは API に頼むだけで、配送・署名・再試行・結果の通知は Resend 側が行います。全体の流れは次のとおりです。

flowchart TD
    A[自分のアプリ] -->|POST /emails<br>APIキー + JSON| R[Resend API]
    R -->|DKIM署名を付けて配送| M[受信側のメールサーバー]
    M --> U[受信者の受信箱]
    D[自分のドメインのDNS<br>SPF・DKIM・DMARC] -.->|照合| M
    R -.->|Webhook<br>email.delivered など| A

よく調べられることの答えを先にまとめます。

知りたいこと答え詳しい節
Resendとは開発者向けのメール送信API。SDK と SMTP の両方で送れるResendとは
Resend 料金無料は月3,000通・1日100通。Pro は月$20で月5万通Resendの料金
Resend 使い方APIキー作成→ドメイン追加→DNS 登録→SDK で送信使い方5ステップ
403で送れないfrom のドメインが未認証。onboarding@resend.dev は自分宛てだけエラーの形
429が返る既定は1秒10リクエスト。retry-after を待って送り直すレート制限と再送
二重送信を防ぐIdempotency-Key ヘッダー(24時間・1〜256文字)レート制限と再送
HTMLメールを作るReact Email でコンポーネントから HTML とテキストを作るReact Email
届いたか知りたいWebhook(email.delivered など)を署名検証して受けるWebhook
開発中のメール確認Mailpit や MailDev で SMTP を手元で受ける開発中のメール確認

確認環境:Ubuntu 24.04.5 LTS・Node.js 22.23.4・Python 3.13.18 の上で、作業用のフォルダーに Resend の公式 SDK(npm の resend 6.35.0、PyPI の resend 2.50.1)と react-email 6.11.6(react 19.3.1)を入れて動かしました。Resend には実際に送信しておらず、アカウントも作っていません。SDK の送信先は、SDK のベース URL を差し替える設定(Node.js は baseUrl、Python は環境変数 RESEND_API_URL)をソースコードで確かめてから、127.0.0.1 で動かした自作のモックサーバーに向けました。モックは公式のエラー一覧(HTTP ステータスと name)と SDK の型定義(statusCode・message・name)に合わせて作ったもので、message の文言はモック独自です。Webhook は自作の受け口で SDK の webhooks.verify を使って検証し、開発用のメール受け口は MailDev 3.0.0 を 127.0.0.1 で起動して nodemailer 10.3.1 と Python の smtplib から送りました(Mailpit はこの環境に取得できなかったため、公式ドキュメントで確かめた範囲を書いています)。料金・制限・DNS・リージョン・各社の料金は公式ページで確かめました(確認日 2026年10月29日)。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Resendとは|使い方・料金とメール送信APIのエラー・Webhook実例」

Resendとは(メール送信APIの役割)

この節では、Resend が何をするサービスで、自分でメールサーバーを持つのと何が違うのかが分かります。

Resend は、アプリが送る「トランザクションメール」(会員登録・パスワード再設定・注文確認のように、操作をきっかけに1通ずつ送るメール)を、API 経由で代わりに届けるクラウドサービスです。連絡先リストへの一斉配信(Broadcasts)も用意されています。公式ドキュメントのクイックスタートには Node.js・Next.js・Express・PHP・Laravel・Python・Ruby・Rails・Go・Rust・Elixir・Java・.NET・CLI が並んでいます。

メールは「送信ボタンを押せば届く」ように見えますが、実際には、送信元のドメインが本物かを受信側が DNS で確かめ、怪しいものは迷惑メールに回されます。自前のメールサーバーで送ると、この認証の設定、IP アドレスの評判、配送の再試行、届かなかったメールの記録をすべて自分で面倒を見ることになります。Resend はそこを引き受け、開発者は API を呼ぶ側に集中できます。

自前の SMTP サーバーで送る

Postfix などの構築・更新・監視が要る

IP の評判づくりと迷惑メール対策を自分で行う

届いたか・跳ね返ったかを自分で集計する

Resend のようなメール送信APIで送る

HTTPS で JSON を送るだけ(SMTP も可)

DKIM 署名と配送は Resend が行う

結果はログと Webhook で受け取れる

この記事で動かした範囲の数字をまとめると、次のとおりです。

10回/秒既定のレート制限チーム単位で全 API キー共通
3,000通/月無料プランの送信数1日100通まで(UTC 0時に戻る)
4,677バイトReact Email の出力見出し・本文・ボタン1つのメール
8回Webhook の送信試行失敗が続くと最後は約27時間後

Resendの料金(無料枠と有料プラン)

この節では、Resend の無料プランでどこまで送れるかと、有料プランの料金・超過料金が分かります。

トランザクションメールの料金は、Free($0)・Pro(月$20)・Scale(月$90)・Enterprise(個別見積り)の4段階です。公式の料金ページ(2026年10月29日時点)の値を表にしました。

プラン月額月の送信数1日の上限主な違い
Free$03,000通100通独自ドメイン3・Webhook エンドポイント1・データ保持30日・超過課金なし
Pro$2050,000通なし独自ドメイン10・超過は1,000通ごとに$0.90
Scale$90100,000通なし独自ドメイン1,000・専用IP は月$30のアドオン(1日3,000通を超える送信者が対象)
Enterprise個別見積り契約によるなしレート制限・ドメイン数などを個別に決める
  • 1日の上限は UTC の暦日で数えます。毎日 UTC 0時(日本時間の午前9時)に戻り、24時間の移動枠ではありません。有料プランには1日の上限が無く、月の上限だけです。
  • 超過料金は有料プランだけで、1,000通の束ごとに自動で課金されます。Free は上限で止まり、daily_quota_exceeded か monthly_quota_exceeded(429)が返ります。
  • 使った量は応答ヘッダーの x-resend-daily-quota(Free のみ)と x-resend-monthly-quota で分かります。
  • テスト用アドレス(delivered@resend.dev など)への送信も、送信数に数えられます。

計算の例です。月に6万通送るなら、Pro の$20に超過1万通分(10束×$0.90=$9)を足して月$29で、Scale の$90より安くなります。月10万通を超えるあたりから Scale を比べる、という目安になります。

Resendの使い方:送信までの5ステップ

この節では、Resend でメールを送れるようになるまでの順番と、各ステップで気をつける点が分かります。

使い方は「APIキーを作る→自分のドメインを追加→DNS に登録して認証→SDK で送る→結果を受け取る」の順です。画面の操作は公式ドキュメントで確かめた範囲で書いています。

1

アカウントと API キーを作る

ダッシュボードの API Keys でキー(re_ で始まる文字列)を作る。送信だけに使うキーは権限を絞っておくと、漏れたときの被害が小さい。キーは環境変数 RESEND_API_KEY に入れ、コードやリポジトリに書かない。

ダッシュボード

2

送信に使うドメインを追加する

Domains で、ルートドメインではなく mail.example.com のようなサブドメインを追加するよう公式は強く勧めている。送信の評判を本体のドメインと分けられる。送信リージョンは北バージニア・アイルランド・サンパウロ・東京(ap-northeast-1)から選ぶ。

ドメイン

3

DNS にレコードを登録して認証する

Records タブに出た DKIM・SPF 用のレコード(TXT と MX)を、DNS 事業者の画面にコピーして登録する。多くは15分以内に認証済み(Verified)になり、伝わるまで最大72時間かかることがある。

DNS

4

SDK を入れて送る

Node.js なら npm install resend、Python なら pip install resend。from に認証済みドメインのアドレスを書いて emails.send を呼ぶ。認証前は onboarding@resend.dev から自分のアドレス宛てにだけ送れる。

コード

5

ログと Webhook で結果を確かめる

Emails 画面で1通ごとの状態を見られる。アプリで扱うなら Webhook を登録し、email.delivered・email.bounced などを受け取る。

運用

Resendでメールを送る(Node.js・Python・curl の送信リクエスト)

この節では、SDK が実際にどんな HTTP リクエストを送り、どんな値を返すのかが分かります。

中身は、https://api.resend.com/emails への POST で、Authorization: Bearer APIキー と JSON の本文だけです。Node.js の SDK で送ったときにモックが受けたリクエストを、そのまま載せます。

import { Resend } from "resend";

// 本番では new Resend(process.env.RESEND_API_KEY) だけでよい。
// この記事では送信先を 127.0.0.1 の自作モックにするため baseUrl を指定した
const resend = new Resend("re_mock_0000000000000000", { baseUrl: "http://127.0.0.1:41370" });

const { data, error, headers } = await resend.emails.send({
  from: "Acme <noreply@mail.example.com>",
  to: ["user@example.org"],
  subject: "ようこそ",
  html: "<p>登録ありがとうございます</p>",
});
if (error) {
  console.error(error.statusCode, error.name, error.message);
} else {
  console.log(data.id, headers["ratelimit-remaining"]);
}

モックのログに残ったリクエストです。

POST /emails HTTP/1.1
host: 127.0.0.1:41370
Authorization: Bearer re_moc...(ログでは途中で切った)
User-Agent: resend-node:6.35.0
Content-Type: application/json
accept: */*
content-length: 146

{"from":"Acme <noreply@mail.example.com>","html":"<p>登録ありがとうございます</p>","subject":"ようこそ","to":["user@example.org"]}
baseUrl = http://127.0.0.1:41370
1 html  : {"data":{"id":"544bb9b9-0b74-4764-9b6c-0c7993c78648"},"error":null} ratelimit-remaining=9
2 react : {"data":{"id":"844461ff-7524-47f8-91ea-53a6e2eff75b"},"error":null} ratelimit-remaining=9

1Node.js は例外を投げない

失敗しても { data: null, error, headers } が返る。error を見ずに先へ進むと、送れていないことに気づけない。

2Python は例外を投げる

4xx・5xx で ResendError の仲間(MissingRequiredFieldsError など)が飛ぶ。try で受ける。

3User-Agent が名乗りになる

Node.js は resend-node:6.35.0、Python は resend-python:2.50.1 を送っていた。

Python の SDK で送る

Python の SDK は、モジュールを読み込んだ時点で環境変数 RESEND_API_URL を読みます(無ければ https://api.resend.com)。差し替えるなら import より前に設定します。

import os
os.environ["RESEND_API_URL"] = "http://127.0.0.1:41370"  # 自作モック。import より前に設定する
import resend

resend.api_key = "re_mock_0000000000000000"
r = resend.Emails.send({
    "from": "Acme <noreply@mail.example.com>",
    "to": ["user@example.org"],
    "subject": "ようこそ",
    "html": "<p>登録ありがとうございます</p>",
})
print(r["id"])

try:
    resend.Emails.send({"from": "noreply@mail.example.com", "to": "user@example.org", "html": "<p>x</p>"})
except resend.exceptions.ResendError as e:
    print(type(e).__name__, e.code, e.error_type, e.message)
api_url = http://127.0.0.1:41370
ok   : {'id': '3b45ca00-b605-4392-8240-bee604ef65ad', 'http_headers': {..., 'ratelimit-limit': '10', 'ratelimit-remaining': '9', 'ratelimit-reset': '1'}}
domain: ResendError | code 403 | type validation_error | The gmail.com domain is not verified.
nosubj: MissingRequiredFieldsError | code 422 | type missing_required_field | Missing `subject` field.

curl で直接送る

SDK を使わない言語でも、同じ形の HTTP リクエストを送れば動きます。公式の API リファレンスでは、User-Agent ヘッダーが無いリクエストは403で断られるとされています。curl や多くの HTTP クライアントは自動で付けますが、自作のクライアントでは付け忘れに気をつけます。

$ curl -v http://127.0.0.1:41370/emails \
  -H 'Authorization: Bearer re_mock_0000000000000000' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: welcome-user/123456789' \
  -d '{"from":"Acme <noreply@mail.example.com>","to":["user@example.org"],"subject":"ようこそ","html":"<p>こんにちは</p>"}'
> POST /emails HTTP/1.1
> Host: 127.0.0.1:41370
> User-Agent: curl/8.5.0
> Authorization: Bearer re_mock_0000000000000000
> Content-Type: application/json
> Idempotency-Key: welcome-user/123456789
> Content-Length: 125
>
< HTTP/1.1 200 OK
< Content-Type: application/json
< Content-Length: 46
< ratelimit-limit: 10
< ratelimit-remaining: 9
< ratelimit-reset: 1
<
{"id": "f187d00f-c568-42c6-9f5a-998226e75191"}

送信でよく使うパラメーター

名前必須中身・上限(公式)
from必須送信元。名前 <アドレス> で表示名を付けられる
to必須宛先。配列で複数(50件まで)
subject必須件名
html / textどちらかtext を省くと HTML から作られる。空文字で作らない指定になる
react任意React のコンポーネント(Node.js の SDK だけ)
cc / bcc / reply_to任意文字列か配列
attachments任意content(Base64)か path。1通あたり Base64 にして40MBまで
tags任意name と value。英数字・_・- だけで256文字まで
headers任意メールに足すヘッダー

送信時刻の予約(in 1 min のような自然な英語か ISO 8601 で指定)や、ダッシュボードで作ったテンプレートを ID で指定して送る機能もあります。テンプレートを使うときは html・text・react は送れません。

Resend APIのエラーとレスポンスの形

この節では、Resend のエラー応答の形と、HTTP ステータスと name ごとの原因・直し方が分かります。

エラーは {"statusCode": 403, "message": "…", "name": "validation_error"} の形で返り、プログラムでは name で分岐します。同じ validation_error が400・401・403で使われるので、ステータスと組で見ます。モックで再現した応答です(message はモック独自の文言、7だけは SDK が出した実物)。

3 domain: {"data":null,"error":{"statusCode":403,"message":"The gmail.com domain is not verified.","name":"validation_error"}}
4 test  : {"data":null,"error":{"statusCode":403,"message":"You can only send testing emails to your own email address (owner@example.net).","name":"validation_error"}}
5 nosubj: {"data":null,"error":{"statusCode":422,"message":"Missing `subject` field.","name":"missing_required_field"}}
6 badkey: {"data":null,"error":{"statusCode":401,"message":"API key is invalid.","name":"validation_error"}}
7 nokey  : Missing API key. Pass it to the constructor `new Resend("re_123")`

公式のエラー一覧から、送信でぶつかりやすいものを抜き出しました。

HTTPname起きる場面直し方
400invalid_idempotency_key冪等キーが1〜256文字の外長さを直す
401missing_api_keyAuthorization ヘッダーが無いBearer でキーを付ける
401restricted_api_key送信専用キーで送信以外を呼んだ権限のあるキーを使う
400・401validation_errorキーが無効・削除済みキーを作り直す
403validation_errorfrom のドメインが未認証ドメインを認証し from を合わせる
403validation_errorテスト用ドメインから他人に送った自分宛てにするか、ドメインを認証する
409invalid_idempotent_request同じキーで本文を変えたキーか本文を直す
422missing_required_fieldfrom・to・subject などが無いmessage の欄名を見て足す
422invalid_attachment添付に content も path も無いどちらかを付ける
429rate_limit_exceeded1秒あたりの回数を超えた待って送り直す・キューに入れる
429daily_quota_exceeded1日の上限を超えたUTC 0時を待つかプランを上げる
500application_errorResend 側の想定外のエラー時間をおいて再試行・状態ページを見る

SDK の段階で止まるエラーもあります。Node.js の SDK は、キーを渡さず環境変数 RESEND_API_KEY も無いと、HTTP を送る前にコンストラクターで Missing API key. Pass it to the constructor `new Resend("re_123")` を投げました。また応答が JSON として読めないときは、SDK が自分で application_error を組み立てて返す作りになっていました(6.35.0 のソースで確認)。

レート制限と再送(429・Idempotency-Key)

この節では、429が返る条件と正しい送り直し方、二重送信を防ぐ Idempotency-Key の動きが分かります。

既定のレート制限は、チーム単位で1秒あたり10リクエストです。応答には ratelimit-limit・ratelimit-remaining・ratelimit-reset・retry-after のヘッダーが付きます。モックを同じ規則にして、秒の頭で15通を同時に送ったところ、10通が成功し5通が429になりました。続けて、retry-after を守って送り直す関数で12通送ると、全部届きました。

burst15: {"ok":10,"rate_limit_exceeded":5}
error: {"statusCode":429,"message":"Too many requests. You can only make 10 requests per second.","name":"rate_limit_exceeded"}
headers: {"ratelimit-limit":"10","ratelimit-remaining":"0","ratelimit-reset":"1","retry-after":"1"}
  429 -> 1000ms 待って再送 (1回目)
  429 -> 1000ms 待って再送 (1回目)
retry: ok= 12 tries= [1,1,1,1,1,1,1,1,1,2,2,1]
async function sendWithRetry(payload, max = 3) {
  for (let i = 0; ; i++) {
    const r = await resend.emails.send(payload);
    if (!r.error || r.error.name !== "rate_limit_exceeded" || i >= max) return r;
    const wait = Number(r.headers?.["retry-after"] ?? 1) * 1000; // 応答の retry-after(秒)を守る
    await new Promise((ok) => setTimeout(ok, wait));
  }
}

SDK(Node.js 6.35.0)には、429で自動的に送り直す処理がありません。ソースを見ても再送の処理は無く、429は error として返ってくるだけでした。たくさん送る処理は、上のような待ち合わせか、キュー(ジョブを1件ずつ流す仕組み)に入れます。

Idempotency-Key で二重送信を防ぐ

通信が途中で切れたとき、送り直すと同じメールが2通届くおそれがあります。Idempotency-Key ヘッダー(SDK では idempotencyKey)を付けると、Resend は24時間そのキーを覚え、同じ本文の再送には最初と同じ ID を返して送りません。公式の仕様どおりに作ったモックでの結果です。

const p = { from: "noreply@mail.example.com", to: "user@example.org", subject: "注文確認", html: "<p>注文 123</p>" };
await resend.emails.send(p, { idempotencyKey: "order-confirm/123" });                      // 1回目
await resend.emails.send(p, { idempotencyKey: "order-confirm/123" });                      // 同じ本文で再送
await resend.emails.send({ ...p, subject: "変更" }, { idempotencyKey: "order-confirm/123" }); // 本文を変えた
8 idem#1: {"data":{"id":"e16eaabe-dd7e-45b1-a296-bf6b528fc3a4"},"error":null}
8 idem#2: {"data":{"id":"e16eaabe-dd7e-45b1-a296-bf6b528fc3a4"},"error":null}
8 idem#3: {"data":null,"error":{"statusCode":409,"message":"Same idempotency key used with a different request payload.","name":"invalid_idempotent_request"}}
(257文字のキー){"statusCode": 400, "message": "The key must be between 1-256 chars.", "name": "invalid_idempotency_key"} 400

429が返ったとき

やってはいけないすぐに同じ処理を何度も呼び直す。枠が空かないまま429を重ねる
→
代わりにretry-after の秒数だけ待ち、回数に上限を付ける。量が多ければキューで流す

既定の枠は1秒10回で、全 API キー共通

送り直すとき

やってはいけない送り直すたびに新しい乱数のキーを作る。二重送信を防げない
→
代わりにwelcome-user/123456789 のように「何の・どれの」メールかで決まるキーにする

公式の例は <イベント種別>/<対象ID> の形

React Emailでメール本文を作る

この節では、React Email で HTML メールを作る方法と、実際に出てくる HTML・テキストの中身が分かります。

React Email は、メールの本文を React のコンポーネントで書き、メールソフトで崩れにくい HTML とテキストに変換するライブラリです。Resend と同じ開発元が作っていて、Node.js の SDK は react を渡すと自動で HTML に変換して送ります。公式の手順では react-email を入れます。

$ npm install -E react-email react react-dom
$ npm install -D -E @react-email/ui
# package.json の scripts に "email:dev": "email dev" を足してプレビュー(localhost:3000)
$ npm run email:dev

2026年10月29日時点で @react-email/components を入れたところ、npm が @react-email/button などに「Package no longer supported」の警告を出しました。公式の手順どおり、部品は react-email から読み込みます。作ったテンプレートです。

import { Html, Head, Preview, Body, Container, Heading, Text, Button, Hr } from "react-email";

export default function Welcome({ name = "山田さん", url = "https://app.example.com/start" }) {
  return (
    <Html lang="ja">
      <Head />
      <Preview>登録ありがとうございます。最初の設定は3分で終わります。</Preview>
      {/* 日本語のメールでは <Body lang="ja" ...> とする(下の「lang の注意」) */}
      <Body style={{ backgroundColor: "#f6f6f6", fontFamily: "sans-serif" }}>
        <Container style={{ backgroundColor: "#ffffff", padding: "24px", maxWidth: "480px" }}>
          <Heading as="h2">{name}、ようこそ</Heading>
          <Text>アカウントの登録が完了しました。下のボタンから最初の設定を始められます。</Text>
          <Button href={url} style={{ backgroundColor: "#e88e2c", color: "#fff", padding: "12px 20px", borderRadius: "6px" }}>
            設定を始める
          </Button>
          <Hr />
          <Text style={{ fontSize: "12px", color: "#666" }}>このメールは送信専用です。</Text>
        </Container>
      </Body>
    </Html>
  );
}

HTML とテキストに変換します。

import { render } from "react-email";
import Welcome from "./emails/Welcome";

const html = await render(<Welcome name="山田さん" />);                       // 送信用(1行)
const pretty = await render(<Welcome name="山田さん" />, { pretty: true });   // 目で見る用
const text = await render(<Welcome name="山田さん" />, { plainText: true });  // テキスト版
html bytes 4677 pretty bytes 5676 text bytes 270 ms(3 renders) 212
---- text ----
山田さん、ようこそ

アカウントの登録が完了しました。下のボタンから最初の設定を始められます。

設定を始める https://app.example.com/start

----------------------------------------

このメールは送信専用です。

出てきた HTML の本文部分(抜粋)です。<Container> は role="presentation" の表に、<Button> は Outlook 向けの余白指定(mso- で始まる CSS)を持つリンクになっていました。CSS は style 属性に書き込まれ、外部の CSS を読めないメールソフトでも同じ見た目になります。

<table align="center" width="100%" border="0" cellpadding="0" cellspacing="0"
  role="presentation" style="max-width:480px;background-color:#ffffff">
  <tbody>
    <tr style="width:100%">
      <td style="padding:24px">
        <h2>山田さん、ようこそ</h2>
        <p style="font-size:14px;line-height:24px;margin-top:16px;margin-bottom:16px">
          アカウントの登録が完了しました。…</p>
        <a href="https://app.example.com/start"
          style="line-height:100%;text-decoration:none;display:inline-block;max-width:100%;
                 mso-padding-alt:0px;background-color:#e88e2c;color:#fff;padding:12px 20px;..."
          target="_blank">(Outlook 用の条件付きコメント)<span style="...;mso-text-raise:9px">設定を始める</span></a>
        <hr style="width:100%;border:none;border-color:transparent;border-top:1px solid #eaeaea" />
  • プレビュー文:<Preview> の文は、受信一覧の件名の横に出る1行です。HTML の先頭に見えない形で置かれ、後ろに本文が混ざらないよう幅0の文字が172個続いていました。
  • lang の注意:<Html lang="ja"> だけでは、body と表のセルに lang="en" が出ました。<Body lang="ja"> と書くと lang="ja" に変わることを確かめています。読み上げソフトの読み方に関わるので、日本語のメールでは Body にも付けます。
  • テキスト版:plainText: true で、ボタンが「設定を始める https://…」の形に、Hr が線に変わりました(270バイト)。

SDK に react を渡して送ると、モックが受けた本文には react というキーは無く、変換済みの html が入っていました。text は送られていないので、Resend 側で HTML から作られます。

await resend.emails.send({
  from: "Acme <noreply@mail.example.com>",
  to: "user@example.org",
  subject: "ようこそ",
  react: <Welcome name="山田さん" />,
  tags: [{ name: "category", value: "welcome" }],
});
// モックが受けた本文(先頭):
// {"from":"Acme <noreply@mail.example.com>","html":"<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" ...
//  ...","subject":"ようこそ","tags":[{"name":"category","value":"welcome"}],"to":"user@example.org"}
// content-length: 4903(react というキーは送られず、html に変換されていた)

Resendのドメイン認証(SPF・DKIM・DMARC)の意味

この節では、Resend のドメイン認証で登録する DNS レコードが何を意味し、DMARC をどう進めるかが分かります。

ドメイン認証は、「このドメインのメールを Resend が送ってよい」と受信側に証明する設定です。これが無いと、受信側は送信元を確かめられず、迷惑メールにされるか受け取りを断ります。3つの仕組みの役割は次のとおりです。

仕組みDNS の種類何を証明するかResend での扱い
SPFTXT(+MX)このサーバーから送ってよい、という許可の一覧Return-Path 用のサブドメイン(既定は send)に登録
DKIMTXT本文が途中で書き換えられていないことを、電子署名で示すResend が署名し、公開鍵を DNS に置く
DMARCTXT(_dmarc)SPF・DKIM に失敗したメールをどう扱うかの方針ドメイン認証の後に自分で設定する

レコードの名前と値は、ダッシュボードの Records タブに出たものを、そのままコピーして貼ります(1文字でも違うと認証されません)。Cloudflare で CNAME を使う場合は、プロキシ(橙色の雲)を切ります。DMARC は公式が勧める次の形から始めます(値は例)。

# 例:mail.example.com から送る場合(値はダッシュボードの Records タブに出たものをそのまま貼る)
_dmarc.example.com.   TXT   "v=DMARC1; p=none; rua=mailto:dmarcreports@example.com;"

# 登録できたかを手元で確かめる
$ dig +short TXT _dmarc.example.com
$ dig +short MX send.mail.example.com

DMARC の方針は、いきなり厳しくせず、段階を踏んで上げます。

1

p=none で見るだけ

全部受け取らせ、rua に届く集計レポートで失敗を見る

2

全部の送信元で dmarc=pass を確かめる

Resend 以外に、社内の別システムやフォームの通知も、そのドメインで送っていないか洗い出す

3

p=quarantine にする

失敗したメールは迷惑メールフォルダーへ

4

p=reject にする

失敗したメールは受け取りを断られる。正規のメールが全部通ると確信してから

リージョンは送信元の場所を決めるもので、アカウントのデータは選んだリージョンに関係なく米国に保存される、と公式に書かれています。社内の決まりで保存場所が問われる場合は、この点を確かめておきます。

ResendのWebhookで配信結果を受け取る(署名検証)

この節では、Resend の Webhook で届いた本文の形と、署名の検証で何がはじかれるかが分かります。

Webhook は、メールが届いた・跳ね返った・迷惑メールと報告された、といった出来事を、Resend が自分のサーバーへ HTTPS で知らせる仕組みです。メールに関する主なイベントは次のとおりです。

イベント起きるとき
email.sentAPI が受け付け、配送を試みる段階になった
email.delivered受信側のメールサーバーへの配送に成功した
email.delivery_delayed一時的な問題で配送が遅れている
email.bounced受信側が恒久的に拒否した(宛先が無いなど)
email.complained届いたが、受信者が迷惑メールとして報告した
email.failed・email.suppressedエラーで送れなかった・抑止リストにより送らなかった
email.opened・email.clicked開封・リンクのクリック(追跡を有効にした場合)

ドメイン・連絡先・抑止リストなどのイベントもあります。署名は Svix(Webhook 配信の仕組み)の方式で、svix-id・svix-timestamp・svix-signature の3つのヘッダーで届きます。公式の delivered の例と同じ構造の本文に、手元で作った秘密鍵(whsec_ で始まる)で署名し、自作の受け口に送りました。

POST / HTTP/1.1
Content-Type: application/json
svix-id: msg_t137_001
svix-timestamp: 1793363040
svix-signature: v1,LMn8pV23qoEJLakRNmRdkPajzRoLl5BB/iyWOBgUyJo=

{"type":"email.delivered","created_at":"2026-10-29T03:12:05.000Z","data":{"created_at":"2026-10-29T03:12:01.000Z",
 "email_id":"544bb9b9-0b74-4764-9b6c-0c7993c78648","from":"Acme <noreply@mail.example.com>","to":["user@example.org"],
 "subject":"ようこそ","tags":{"category":"welcome"}}}

受け口のコードです。SDK の webhooks.verify に、受け取ったままの本文を渡します。

import { createServer } from "node:http";
import { Resend } from "resend";

const resend = new Resend(process.env.RESEND_API_KEY);
const seen = new Set();   // 本番では DB などに残す

createServer((req, res) => {
  let raw = "";
  req.setEncoding("utf8");
  req.on("data", (c) => (raw += c));
  req.on("end", () => {
    const id = req.headers["svix-id"];
    try {
      const event = resend.webhooks.verify({
        payload: raw,   // JSON.parse する前の、生の本文
        headers: { id, timestamp: req.headers["svix-timestamp"], signature: req.headers["svix-signature"] },
        webhookSecret: process.env.RESEND_WEBHOOK_SECRET,
      });
      if (seen.has(id)) return res.writeHead(200).end("duplicate");   // 再送は同じ svix-id で来る
      seen.add(id);
      // event.type === "email.delivered" などで処理を分ける
      res.writeHead(200).end("ok");
    } catch {
      res.writeHead(400).end("Invalid webhook");
    }
  });
}).listen(41371, "127.0.0.1");

正しい署名のほか、わざと崩した5通りを送った結果です。

valid      -> 200 ok
retry      -> 200 duplicate
tampered   -> 400 Invalid webhook
reformat   -> 400 Invalid webhook
stale      -> 400 Invalid webhook
wrongkey   -> 400 Invalid webhook

(受け側のログ)
{"id":"msg_t137_001","result":"ok","type":"email.delivered","email_id":"544bb9b9-...","to":["user@example.org"]}
{"id":"msg_t137_001","result":"duplicate"}
{"id":"msg_t137_002","result":"rejected","error":"No matching signature found"}
{"id":"msg_t137_003","result":"rejected","error":"No matching signature found"}
{"id":"msg_t137_004","result":"rejected","error":"Message timestamp too old"}
{"id":"msg_t137_005","result":"rejected","error":"No matching signature found"}
送ったもの結果分かること
正しい署名200type・email_id・to を取り出せた
同じ svix-id でもう一度200(処理しない)再送は同じ ID で来るので、ID で重複を除く
本文の宛先を書き換え400本文が1文字でも違えば通らない
同じ内容を整形し直した JSON400JSON.parse してから文字列に戻すと失敗する
10分前の時刻400許容は前後5分(SDK が使う standardwebhooks 1.1.1 の定数)
別の秘密鍵400鍵は Webhook ごとに違う

SDK の中では、svix の3つのヘッダーを Standard Webhooks の名前(webhook-id など)に置き換えて検証していました。SDK を使えない言語では、Svix の公式手順どおりに自分で計算します。

import base64, hashlib, hmac

def expected_signature(secret: str, msg_id: str, timestamp: str, raw_body: str) -> str:
    key = base64.b64decode(secret.split("_", 1)[1])          # "whsec_" の後ろを Base64 デコード
    signed = f"{msg_id}.{timestamp}.{raw_body}".encode()      # id.timestamp.本文 を連結
    return "v1," + base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode()

# svix-signature はスペース区切りで複数入ることがある。どれか1つと
# hmac.compare_digest で一致すれば本物。時刻のずれも確かめる

受け口が200番台を返さないと、Resend は送り直します。公式の予定では、即時・5秒後・5分後・30分後・2時間後・5時間後・10時間後・10時間後の計8回で、最後は最初から27時間35分5秒後です。ダッシュボードから成功・失敗したメッセージを再送(Replay)することもできます。開発中に手元の受け口へ届けたいときは、公式の CLI でローカルにイベントを受ける方法が案内されています。外部から手元のサーバーへつなぐ方法は、関連記事「ngrokとは|使い方・料金・Webhook受信とエラーの直し方」で扱っています。署名検証・リトライ・冪等性を一般的な仕組みから学ぶなら、関連記事「Webhookとは?実装までわかる|署名検証・リトライ・冪等性を実行ログで解説」で扱っています。

開発中のメール確認(Mailpit・MailDev・テスト用アドレス)

この節では、開発中に本物の宛先へ送らずにメールを確かめる2つの方法が分かります。

確かめ方は2つあります。手元で SMTP を受けて見た目を確かめる方法(Mailpit・MailDev)と、Resend のテスト用アドレスに送って配送結果や Webhook を確かめる方法です。実在しない宛先や他人のアドレスに試しに送ると、跳ね返りが増えて送信の評判を下げるため、公式もこの2つを勧めています。

Resend のテスト用アドレス

アドレス再現される結果
delivered@resend.dev配送成功
bounced@resend.dev受信側が拒否(SMTP 550 5.1.1 Unknown User)
complained@resend.dev届いたが迷惑メールとして報告された
suppressed@resend.dev以前に跳ね返ったとして送信が抑止される

delivered+signup@resend.dev のように + の後ろに文字を足すと、テストの種類ごとに Webhook を見分けられます(suppressed は不可)。送信数には数えられます。

Mailpit で手元に受ける

Mailpit は、SMTP で受けたメールをブラウザーの画面と API で見られる開発用のツールです。公式ドキュメントでは、Mac は Homebrew、Linux・Windows は配布されている実行ファイル、Docker のイメージも用意されています。既定のポートは SMTP 1025・画面 8025 です。

# Mac(Homebrew)
$ brew install mailpit
$ mailpit --smtp 127.0.0.1:1025 --listen 127.0.0.1:8025
# 既定は SMTP 0.0.0.0:1025、画面と API 0.0.0.0:8025。手元だけで使うなら 127.0.0.1 を指定する

MailDev で受けた実物

この環境では Mailpit の配布元に接続できなかったため、同じ役割の MailDev 3.0.0(npm で入る)を 127.0.0.1 で起動しました。MailDev も既定では SMTP が全アドレス(::)、画面が 0.0.0.0 で待ち受けるので、手元だけにするには明示します。転送(relay)の設定はしていないので、外には出ていきません。

$ npx maildev --smtp 41372 --ip 127.0.0.1 --web 41373 --web-ip 127.0.0.1
MailDev
  Version: 3.0.0
  SMTP Server
    Address: 127.0.0.1
    Port:    41372
  Web Interface
    URL:     http://127.0.0.1:41373/

React Email で作った HTML とテキストを、nodemailer で送りました。

import nodemailer from "nodemailer";
import { render } from "react-email";
import Welcome from "./emails/Welcome";

// 開発中は手元の受け口へ。本番は smtp.resend.com(ユーザー名 resend・パスワードは API キー)に差し替える
const transport = nodemailer.createTransport({ host: "127.0.0.1", port: 41372, secure: false, ignoreTLS: true });
const el = <Welcome name="山田さん" />;
const info = await transport.sendMail({
  from: "Acme <noreply@mail.example.com>", to: "user@example.org", subject: "ようこそ(開発環境)",
  html: await render(el), text: await render(el, { plainText: true }),
  headers: { "Resend-Idempotency-Key": "welcome-user/123456789" },
});
console.log(info.response);
response: 250 Message queued as 5pkc4mw6 | accepted: [ 'user@example.org' ]

(Python smtplib で送ったときのやり取り。set_debuglevel(1) の出力を抜粋)
send: 'ehlo [127.0.0.1]\r\n'
reply: b'250-PIPELINING\r\n'
reply: b'250-8BITMIME\r\n'
reply: b'250-SMTPUTF8\r\n'
reply: b'250 SIZE 52428800\r\n'
send: 'mail from:<noreply@mail.example.com> size=258\r\n'
reply: b'250 Accepted\r\n'
send: 'rcpt to:<user@example.org>\r\n'
reply: b'250 Accepted\r\n'
send: 'data\r\n'
reply: b'354 End data with <CR><LF>.<CR><LF>\r\n'
reply: b'250 Message queued as fouuhcde\r\n'
send: 'QUIT\r\n'
reply: b'221 Bye\r\n'

受けたメールは API で取り出せます。届いた生のメール(ソース)です。

$ curl -s http://127.0.0.1:41373/api/email/5pkc4mw6/source
Resend-Idempotency-Key: welcome-user/123456789
From: Acme <noreply@mail.example.com>
To: user@example.org
Subject: =?UTF-8?B?44KI44GG44GT44Gd77yI6ZaL55m655Kw5aKD77yJ?=
Message-ID: <8a323f0f-840e-3579-c143-dee4d7ae2673@mail.example.com>
Date: Fri, 30 Oct 2026 12:24:19 +0000
MIME-Version: 1.0
Content-Type: multipart/alternative; ...

Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: base64
...
Content-Type: text/html; charset=utf-8
Content-Transfer-Encoding: quoted-printable
...(全体 11,751 バイト・175 行)

4,677バイトの HTML は、テキスト版(base64)と HTML 版(quoted-printable)の2つを持つ multipart/alternative のメールになり、全体で11,751バイトになりました。日本語の件名は =?UTF-8?B?…?= の形に変換されています。Resend の SMTP に向ける場合は、送り先の設定を差し替えるだけで同じコードが使えます。

ResendをSMTPで使う(smtp.resend.com)

この節では、API ではなく SMTP で Resend を使う設定値と、どんなときに SMTP を選ぶかが分かります。

SMTP しか設定できないアプリ(CMS・既存の業務システム・フレームワークのメール機能)は、送信サーバーを smtp.resend.com にするだけで Resend 経由になります。公式の設定値です。

# 送信サーバー(Resend の SMTP)
host: smtp.resend.com
port: 465(SMTPS)/587(STARTTLS)
user: resend
pass: re_xxxxxxxx(API キー)

ポートは、465・2465が接続直後から暗号化する方式(SMTPS)、25・587・2587が平文で接続してから暗号化に切り替える方式(STARTTLS)です。SMTP で二重送信を防ぐときは、メールのヘッダー Resend-Idempotency-Key にキーを入れます。Python の標準ライブラリで SMTP 送信を書く方法は関連記事「Python smtplibでメール送信|添付・HTML・Gmail対応コード」で、Java の Spring Boot から送る方法は関連記事「Spring Bootでメール送信|HTML・添付・Gmail設定」で扱っています。どちらも、ホスト・ポート・ユーザー名・パスワードを上の値に替えれば Resend で送れます。

Resendと SendGrid・Amazon SES・Postmark・Mailgun の比較と判断表

この節では、主なメール送信サービスの無料枠と最初の有料プランの違い、どれを選ぶかの目安が分かります。

少ない通数から始める新しいアプリなら Resend、AWS の中で大量に安く送るなら Amazon SES、というのが大まかな分かれ目です。各社の公式の料金ページ(2026年10月29日時点)の値です。為替や税は含みません。

サービス無料で使える範囲最初の有料プラン特徴
Resend月3,000通(1日100通)Pro 月$20で月5万通React Email と組み合わせやすい。Webhook・テスト用アドレス
SendGrid60日の無料トライアル(1日100通)Essentials 月$19.95からマーケティングメールの機能も持つ
Amazon SES新規 AWS アカウントのクレジット(最大$200・6か月)従量で1,000通あたり$0.10大量送信で安い。添付は1GBあたり$0.12
Postmark月100通(Developer)Basic 月$15で月1万通トランザクションメールに絞った作り
Mailgun1日100通(Free)Basic 月$15で月1万通Foundation 月$35で月5万通
状況向く選択理由
個人開発・試作で、月に数百〜数千通Resend の Free月3,000通まで無料で、SDK と React Email ですぐ書ける
Next.js などの JS/TS のアプリResendreact をそのまま渡せる。テンプレートを型付きで管理できる
AWS で動いていて月に数十万通以上Amazon SES1,000通$0.10の従量で、通数が多いほど差が開く
通知メールの配送の速さと確実さを最優先Postmark か Resendどちらもトランザクションメール向けの作り
メルマガの作成・配信を担当者の画面で行いたいSendGrid などマーケティング機能の画面が充実している
SMTP しか使えない既存システムどれでも可各社 SMTP の口を持つ。設定値の差し替えで済む

Amazon SES での送信手順は、関連記事「Amazon SESメール送信|AWS CLIとboto3の手順・エラー対処」で扱っています。

Resendのよくあるエラー/つまずき

この節では、Resend を使い始めたときにぶつかりやすいエラーの文言と、原因・直し方が分かります。

症状・文言原因直し方
403 validation_error(from のドメインが未認証)gmail.com など自分が持っていないドメインや、認証前のドメインを from にした認証済みドメインのアドレスを from にする
403 validation_error(テストは自分宛てだけ)onboarding@resend.dev から他人に送った自分のアドレス宛てで試すか、ドメインを認証する
Missing API key. Pass it to the constructor …Node.js の SDK にキーが渡っていない(環境変数の読み込み漏れ)RESEND_API_KEY が実行時に入っているか確かめる
403(エラーコード 1010)自作の HTTP クライアントで User-Agent が無いUser-Agent ヘッダーを付ける
429 rate_limit_exceeded1秒10回を超えた(全キー共通)retry-after を待つ。キューで流す
No matching signature foundWebhook の本文を JSON にしてから検証した・鍵違い生の本文と、その Webhook の秘密鍵で検証する
Message timestamp too oldサーバーの時計のずれ、5分より前のリクエストNTP で時刻を合わせる
Failed to render React component …react を渡したのに描画用の部品が入っていないreact-email(または @react-email/render)を入れる
Top-level await is currently not supported with the "cjs" output formattsx で await をファイルの一番外に書いたが、CommonJS として扱われたpackage.json に "type": "module" を足す
ドメインが Pending のままレコードの値の写し間違い、Cloudflare のプロキシが有効値を貼り直し、72時間たったら Restart verification

表の Top-level await の文言は、この記事の検証で React Email の変換を書いたときに実際に出たものです。

よくある質問

この節では、Resend について調べるときによく出てくる疑問への答えが分かります。

Q. Resend は無料で使えますか?
はい。Free プランは月3,000通・1日100通まで無料です(2026年10月29日時点)。上限を超えると429が返り、自動で課金はされません。

Q. Resend の読み方は?
英語の resend(再送する)と同じ綴りで、「リセンド」と読まれています。

Q. Gmail のアドレスを from にして送れますか?
送れません。from には、自分で DNS を設定して認証したドメインのアドレスを使います。gmail.com を from にすると403の validation_error になります。

Q. ドメインを持っていなくても試せますか?
試せます。onboarding@resend.dev を from にすると、自分のアカウントのアドレス宛てにだけ送れます。

Q. 日本からの送信に向いたリージョンはありますか?
ドメインを追加するときに東京(ap-northeast-1)を選べます。公式は受信者に近いリージョンを勧めています。アカウントのデータ自体は米国に保存されます。

Q. 一度に何人まで送れますか?
1回の送信で to に書けるのは50件までです。多数の宛先に別々の本文で送るときは、まとめて送る Batch の API か、Broadcasts を使います。

Q. 添付ファイルは送れますか?
送れます。attachments に Base64 の content か、置き場所の path を入れます。1通あたり Base64 にした大きさで40MBまでです。

Q. React を使っていなくても React Email は使えますか?
使えます。React Email はメール用の HTML を作るためだけに使えるので、アプリ本体が React でなくても、ビルド時やサーバー側で HTML を作って html として送れます。Python など Node.js 以外の SDK には react の引数がないため、作った HTML を渡します。

Q. 送ったメールが届いたかはどう分かりますか?
ダッシュボードの Emails 画面で1通ごとの状態を見られます。アプリで扱うなら Webhook の email.delivered・email.bounced を受けます。

Q. SendGrid から乗り換えるときに気をつけることは?
ドメイン認証のやり直し(DNS レコードの追加)と、Webhook の受け口の作り直し(署名の方式が違う)が要ります。SMTP で使っているなら、送信サーバーの設定を smtp.resend.com に替えるだけで送れます。

Q. API キーが漏れたらどうしますか?
ダッシュボードですぐにそのキーを削除し、新しいキーを作って環境変数を入れ替えます。送信だけのキーにしておくと、漏れても送信以外の操作はできません。

まとめ

  • Resend は開発者向けのメール送信APIで、POST /emails に Bearer キーと JSON を送るか、smtp.resend.com で送る。
  • 料金は Free が月3,000通・1日100通、Pro が月$20で5万通(2026年10月29日時点)。1日の枠は UTC 0時に戻る。
  • エラーは statusCode・message・name の JSON。Node.js の SDK は例外を投げず error を返し、429でも自動では送り直さない。retry-after と Idempotency-Key で安全に送り直す。
  • React Email は表組みとインライン CSS の HTML と、テキスト版を出す。日本語のメールは Body にも lang="ja" を付ける。
  • Webhook は生の本文で署名を検証し、svix-id で重複を除く。開発中は Mailpit や MailDev とテスト用アドレスで、本物の宛先に送らずに確かめる。

参考・出典

確認日はいずれも 2026年10月29日です。

  • Resend「Pricing」:https://resend.com/pricing
  • Resend Docs「Introduction」:https://resend.com/docs/introduction
  • Resend Docs「API Reference Introduction」:https://resend.com/docs/api-reference/introduction
  • Resend Docs「Send Email」:https://resend.com/docs/api-reference/emails/send-email
  • Resend Docs「Errors」:https://resend.com/docs/api-reference/errors
  • Resend Docs「Rate Limit」:https://resend.com/docs/api-reference/rate-limit
  • Resend Docs「Idempotency Keys」:https://resend.com/docs/dashboard/emails/idempotency-keys
  • Resend Docs「Send test emails」:https://resend.com/docs/dashboard/emails/send-test-emails
  • Resend Docs「Domains」「Add a domain」「Regions」「DMARC」:https://resend.com/docs/dashboard/domains/introduction
  • Resend Docs「Send with SMTP」:https://resend.com/docs/send-with-smtp
  • Resend Docs「Webhooks」「Event types」「Verify webhooks requests」「Retries and replays」:https://resend.com/docs/webhooks/event-types
  • Svix Docs「Verifying payloads manually」:https://docs.svix.com/receiving/verifying-payloads/how-manual
  • React Email「Manual setup」:https://react.email/docs/getting-started/manual-setup
  • Mailpit「Installation」「Runtime options」:https://mailpit.axllent.org/docs/configuration/runtime-options/
  • Amazon SES「Pricing」:https://aws.amazon.com/ses/pricing/
  • Twilio SendGrid「Email API Pricing」:https://www.twilio.com/en-us/products/email-api/pricing
  • Postmark「Pricing」:https://postmarkapp.com/pricing
  • Mailgun「Pricing」:https://www.mailgun.com/pricing/

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Resendとは|使い方・料金とメール送信APIのエラー・Webhook実例」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ
参加受付中

AI駆動開発、 無料セミナー