mcpField
P
portaly-email

portaly-email

github.com/portaly-aiその他252 回インストール2 日前に更新
リポジトリ

portaly-email について

Portaly EmailのトランザクションメールAPIをアプリに組み込むクリエイター向けのスキルです。アカウントとドメインの設定、サーバー側送信、再試行、配信状況の追跡を扱い、ベータ版は招待制です。

主な機能

  1. 01APIキーとドメインの設定
  2. 02サーバー側でのメール送信
  3. 03冪等キーを使った再試行
  4. 04予約・一括メール送信
  5. 05配信状況の定期的な追跡

活用シーン

  1. 01注文の領収書や通知の送信
  2. 02認証メールやパスワード再設定メールの配信
  3. 03バウンスや苦情のあった宛先の確認

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/portaly-ai/portaly-skills --skill portaly-email

インストール後、Claude Code で/portaly-emailを入力して読み込まれたか確認します。

同梱ファイルreferences/api-contract.md

詳細

Portaly Email(ベータ版)

このスキルを使って、クリエイター(通常はエンジニアではありません)などのユーザーが、自分のアプリから送るメール(領収書、確認コード、パスワードのリセット、通知)を Portaly のメール API に接続できるよう支援します。 回答は実務的な内容にし、まず次の手順を示してから、プロジェクトで使われているスタックに合わせた、すぐにコピーできるコードを提示してください。

これはトランザクションメール専用です。1 人のユーザーの操作をきっかけに送信するメールを指します。 ニュースレターや読者向けのお知らせは、この API ではなく Portaly の管理画面(https://portaly.cc/admin/email)から送信します。

ベータ版 — 招待制です。 Portaly Email はベータ版のため、Portaly から招待されたアカウントのみ利用できます。 それ以外のアカウントはすべての呼び出しで 403 FORBIDDEN となり、エージェントにはユーザーに Portaly へ問い合わせるよう案内する以外にできることはありません。メール API キーの作成には、月間クォータが付与される Premium プランも必要です。ユーザーが招待について言及していない場合は、セットアップ作業を始める前にこの点を伝えてください。

API ホスト

const PORTALY_API_HOST = process.env.PORTALY_API_HOST || 'https://portaly.ai'

ユーザーが意図的に本番環境以外の Portaly を指定している場合を除き、PORTALY_API_HOST は未設定のままにしてください。正式な仕様は https://portaly.ai/docs のメールセクションと https://portaly.ai/openapi.json で確認できます。ここにある references/api-contract.md は作業用のコピーです。内容が異なる場合は、公式ドキュメントを優先してください。

クイックスタート

  1. キー — ユーザーが Portaly の管理画面でメール API キーを作成し、.env に PORTALY_EMAIL_API_KEY として設定します。(ワークフローのステップ 1)
  2. まずはサンドボックス — ドメインを認証するまでは、<name>@sandbox.portaly.tw から送信します。送信先はアカウント所有者の認証済みサインイン用メールアドレスのみで、1 日 50 通までです。<name> は 3〜32 文字の英字、数字、.、_、- で、portaly を含まず、noreply、no-reply、support、info、admin などの予約名であってはなりません(400 INVALID_SENDER)。例:orders、hello。コードが動作することを確認するには十分です。
  3. ドメイン — ユーザーが送信元サブドメインを登録して DNS レコードを追加し、キーがそのドメインを対象にしていることを確認します。(ワークフローのステップ 2)
  4. 送信 — サーバー側のコードから Idempotency-Key を指定して送信します。(ワークフローのステップ 3)
  5. レスポンスの処理と配信状況の追跡 — 再試行可能なものは再試行し、クォータ超過時にはアラートを出し、バウンスしたアドレスや苦情が寄せられたアドレスを記録します。(ワークフローのステップ 4〜5)

ワークフロー

1. API キーを取得する(ユーザーの作業)
  • ユーザーは https://portaly.cc/admin/email/api-keys(經營工具 › 信件管理 › API 管理)でキーを作成します。 キーは pem_ で始まり、一度しか表示されません。

  • 権限:送信のみを行うアプリには sending(送信、一覧表示、検索、キャンセル、クォータの確認)で十分です。API 経由でドメインも登録する場合にのみ full を選択してください。

  • キーで送信できるのは、作成時に選択したドメインからのみです。ドメインがまだ認証されていない場合、管理画面のデフォルトは sandbox.portaly.tw のみです。そのため、今作成したキーでは、後から実ドメインを使って送信できません(403 DOMAIN_NOT_ALLOWED)。「全部網域」(後から登録するドメインも含むすべてのドメイン。full キーでは常にこの設定になります)を選択するか、ステップ 2 の完了後にユーザーに別のキーを作成してもらってください。sandbox.portaly.tw のみに制限されたキーは実質的にテスト用のキーです。独立したテストモードはありません。

  • これは Portaly Payment のキーではありません。 pcs_live_ / pcs_test_ キーではメールを送信できず、pem_ キーでは決済エンドポイントを呼び出せません。両方を使うプロジェクトでは、両方のキーを保持してください。

  • ユーザーにキーをチャットに貼り付けるよう求めてはいけません。 ユーザー自身で .env に追加するよう案内してください。

    PORTALY_EMAIL_API_KEY=pem_xxx
    

    process.env.PORTALY_EMAIL_API_KEY で読み取ります。何よりも先に .env が .gitignore に含まれていることを確認してください。キーがチャットに貼り付けられた場合は、管理画面でそのキーを無効化し、新しいキーを作成するよう伝えてください。

  • サーバー側のみで使用してください。 キーをブラウザーのコード、NEXT_PUBLIC_ / VITE_ 変数、モバイルアプリ、またはユーザーに配布されるものに含めてはいけません。キーを持つ人は誰でも、クリエイターのドメインからメールを送信できます。

2. 送信ドメインを選択して確認する
  • メールの送信元にするアドレス(orders@…、no-reply@…)を確認します。メインの受信トレイにバウンスの問題が影響しないよう、メールを受信しないサブドメイン(例:mail.example.com)を推奨します。
  • full キーがある場合は、GET /api/email/domains でバインド済み・検証済みのドメインを確認できます。キーがない場合は、担当者に https://portaly.cc/admin/email/domains(信箱設定)を確認してもらいます。
  • バインドすると(管理画面から、または full キーを使って { "host": "mail.example.com" } を指定した POST /api/email/domains を実行すると)、dnsRecords が返されます。**MAIL FROM サブドメイン用の DKIM CNAME 3 件に加え、MX と TXT が 1 件ずつ含まれます。**担当者が DNS プロバイダーですべてのレコードを追加し、管理画面で「重新檢查」を押します。DNS の反映には数分から数時間かかる場合があります。identityStatus: "verified" と mailFromStatus: "success" になれば準備完了です。メインドメインの既存のレコード(MX、SPF、DKIM)は変更しないでください。Cloudflare では、新しいレコードをプロキシ経由ではなく、DNS のみ(灰色の雲)に設定してください。
  • 送信サブドメインではメールを受信しないため、replyTo にはメールを受信できるアドレス(例:[email protected])を設定します。設定しないと、顧客からの返信が届きません。
  • それまではサンドボックスを使ってビルドとテストを行います。sandbox.portaly.tw の sender.email を使用し、受信者にはアカウント所有者の検証済みサインイン用メールアドレスを指定します。所有者が検証済みメールアドレスを使わずにサインインしている場合(例:電話番号や LINE を使用した場合)、サンドボックスからはメールをまったく配信できません(403 RECIPIENT_NOT_ALLOWED)。後で実際のドメインに切り替える際は、コードではなく設定を変更します。
  • ドメインの検証が完了したら、キーがそのドメインを対象としていることを確認します(手順 1)。対象でない場合、送信時に 403 DOMAIN_NOT_ALLOWED が返されます。
  • mail.portaly.tw などの Portaly ドメインからは絶対に送信しないでください。403 DOMAIN_NOT_ALLOWED が返されます。
3. 送信する

サーバー側コードからのみ、POST {PORTALY_API_HOST}/api/email/emails を実行します。

const PORTALY_API_HOST = process.env.PORTALY_API_HOST || 'https://portaly.ai'

export class PortalyEmailError extends Error {
  readonly status: number
  readonly code: string | undefined
  readonly retryAfterSeconds: number | null

  constructor(status: number, code: string | undefined, message: string | undefined, retryAfterSeconds: number | null) {
    super(message ?? `Portaly email API answered ${status}`)
    this.name = 'PortalyEmailError'
    this.status = status
    this.code = code
    this.retryAfterSeconds = retryAfterSeconds
  }
}

export async function sendEmail(input: {
  to: string
  subject: string
  html: string
  idempotencyKey: string // derive from your own event, e.g. `order-${orderId}-receipt`; letters, digits, `_`, `-`
}) {
  const res = await fetch(`${PORTALY_API_HOST}/api/email/emails`, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.PORTALY_EMAIL_API_KEY}`,
      'Content-Type': 'application/json',
      'Idempotency-Key': input.idempotencyKey,
    },
    body: JSON.stringify({
      sender: { email: process.env.EMAIL_FROM, name: 'Example Shop' },
      replyTo: process.env.EMAIL_REPLY_TO, // the sending subdomain does not receive mail
      recipients: [{ email: input.to }],
      subject: input.subject,
      html: input.html,
      tags: [{ name: 'ref', value: input.idempotencyKey }], // to find this email again (step 4, 409)
    }),
  })
  const body = await res.json().catch(() => ({}))
  if (!res.ok) {
    const retryAfter = res.headers.get('retry-after')
    throw new PortalyEmailError(res.status, body.error?.code, body.error?.message, retryAfter ? Number(retryAfter) : null)
  }
  return body.data.id as string // emsg_…, store it next to your own record
}
  • 必ずビジネスイベントに基づく Idempotency-Key を指定します。試行ごとにランダムな値を使ってはいけません。同じキーで再試行しても二重送信されないため、タイムアウト後も安全に再試行できます。
  • **送信するメールごとにキーを 1 つ指定し、ユーザーごとに 1 つ指定することは避けます。**キーの有効期限はなく、Portaly は本文を比較しません。同じキーを再利用すると、最初のメールの ID が返され、メールは送信されません。order-${orderId}-receipt は適切ですが、password-reset-${userId} は最初のリセット後、以降のリセットを通知なしに破棄します。リセット要求ごとに固有の ID を使用してください。内容が変わる場合は、新しいキーを使います。
  • キーは Portaly アカウントと送信ドメインの範囲で適用されます。ステージング環境と本番環境で同じアカウントとドメインから送信する場合は、キーに環境名のプレフィックスを付けます(prod-order-… など)。そうしないと、環境間で ID が重複した場合に、本番環境でステージング環境のメールが通知なしに再利用されます。
  • すべてのメールにそのキーをタグとして付けます(前述の { name: 'ref', value: <key> })。一覧の結果には Idempotency-Key が含まれないため、特定のメールを再度検索するにはタグが必要です。タグの値に使えるのは英字、数字、_、- のみです。これらの文字でキーを作成してください。
  • EMAIL_FROM と EMAIL_REPLY_TO は設定に保存しておきます。そうすれば、サンドボックスから検証済みドメインへの切り替えは設定変更だけで済みます。
  • 返された id は、対応する注文またはユーザーとともに保存します。後で配信状況を調べる際に必要です。
  • 受信者は、メール 1 件あたり最大 50 人まで指定でき、{ email, type } の形式で記述します。type には to、cc、bcc を指定でき、省略時は to です。これは1 通のメッセージであり、CC の受信者同士は互いのアドレスが見えます。複数の相手に個別のメッセージを送る場合は、POST /api/email/batches を使用します(最大 100 件のメールをバックグラウンドで送信)。
  • scheduledAt(タイムゾーン付き ISO 8601 形式、1 分後から 30 日未満先まで。上限は 30 日から 10 分を引いた日時)を指定すると予約送信になり、202 が返されます。 POST /api/email/emails/{id}/cancel を実行すると、送信前にキャンセルできます。
  • 添付ファイルには、content(base64 形式。ファイルサイズは約 3 MB まで。リクエストの上限は 4.5 MB)または url(Portaly がダウンロードする公開 HTTPS URL)を指定できます。メール 1 件あたりの合計サイズは 10 MB までです。
  • 各フィールドのルールとバッチの形式については、references/api-contract.md を参照してください。
4. レスポンスを処理する

すべてのエラーは { "error": { "code", "message" } } です。message ではなく、code に基づいて分岐してください。

レスポンスコードで行うこと
200 / 202data.id を保存します。
429 RATE_LIMITEDキーごとのレート制限です。Retry-After 秒待って、同じ Idempotency-Key で再試行してください。
500、任意の 503、ネットワークエラーまたはタイムアウト同じ Idempotency-Key で、上限(例:5回)を設けて指数バックオフで再試行します。その後、後続のジョブ用にキューに登録してアラートを出します。二重送信にはなりません。Retry-After の単位は秒です。
502 SEND_OUTCOME_UNKNOWN送信されている可能性があります。同じ Idempotency-Key で再試行してください。Portaly が結果を把握するまでは、その再試行に 409 が返されます。
409 IDEMPOTENCY_KEY_IN_USE同じキーで約10分間まで再試行します。上限を設け、決して無限にループさせないでください。 その後も 409 が続く場合、送信されていない可能性が高いです(そのキーは以後ずっと 409 のままです)。GET /api/email/emails?status=unknown&tag=ref:<key> で探し、人にアラートを出すか、新しいキーで再送信します。その際、重複送信のリスクが多少あることを受け入れてください。
429 SEND_QUOTA_EXCEEDED一時的なエラーではありません。ループで再試行しないでください。 人にアラートを出します(エラーレベルでログに記録し、通知します)。作成者がクォータを追加購入するか、月次リセットまで待ちます。
429 SANDBOX_DAILY_LIMIT_REACHEDサンドボックスメールは1日50通までです。Asia/Taipei の午前0時にリセットされます。
422 RECIPIENT_SUPPRESSEDすべての to アドレスが Portaly の抑制リストに登録されています(ハードバウンスが発生したか、複数の Portaly 送信者からそのアドレスについて迷惑メールの苦情が寄せられています)。再試行しないでください。DB で無効としてマークし、サインイン済みユーザーには新しいアドレスを求めてください。サインアウト状態のフロー(パスワードリセット)では、アカウントの有無が判明しないよう、通常の汎用メッセージを表示してください。
422 MESSAGE_REJECTEDメールサービスがこのメッセージを拒否しました。同じキーでは同じエラーが返され続けるため、修正して新しいキーで送信してください。
403 DOMAIN_NOT_ALLOWEDそのドメインはこのアカウントに紐付けられていない、Portaly のドメインである、またはキーの対象範囲に含まれていません(ステップ1)。作成者に表示し、再試行しないでください。
その他の 403 DOMAIN_*セットアップの問題です。ステップ2を参照してください。作成者に表示し、再試行しないでください。
403 HARD_BOUNCE_LIMIT_REACHEDそのドメインは本日のハードバウンス上限に達しており、通常はその後停止されるまで、残りの時間はブロックされます。そのドメインからの送信を停止し、人にアラートを出して、bounced アドレスを整理してください(ステップ5)。本日は再試行しないでください。
403 RECIPIENT_NOT_ALLOWEDサンドボックスからアカウント所有者以外に送信しようとしています。
その他の 4xxリクエストを修正してください。INVALID_BODY のメッセージは、不正なフィールド(recipients.0.email: …)から始まります。
  • クォータ上限に達しないよう、大量送信の前、または定期的に GET /api/email/quota の data.remaining を確認し、残りが少なくなったら作成者に警告してください。クォータは受信者ごとに消費され、管理画面から送信するブロードキャストと共有されます。サンドボックスメールではクォータは消費されません。
5. 配信状況を追跡する

Webhook はまだ利用できません。10~15 分ごとなどの定期ジョブを実行し、GET /api/email/emails?recipientStatus=bounced,complained&startDate=… で最近の問題を一覧表示し、startAfter=<pagination.nextCursor> を使ってページングしてください。startDate はメールの作成日時で絞り込みます。迷惑メール報告は数日後に届くこともあるため、実行ごとに約 7 日前までさかのぼって確認してください。アプリで scheduledAt やバッチを使っている場合は、recipientStatus=failed,quota_exceeded,domain_limited,suppressed も一覧表示してください。これらの状態は配信予定時刻になると確認され、ブロックされたメールはここにのみ表示されます。

GET /api/email/emails/{id} では、1 通のメールの受信者を確認できます。少数のメールなら問題ありませんが、一覧表示やクォータ確認と合わせて、読み取りリクエストはキーごとに 1 分あたり 120 件を共有します。そのため、アプリが 1 分あたり数十通を超えるメールを送るようになると、メールごとのポーリングでは対応できなくなります。

  • bounced — メールアドレスが存在しません。データベースで無効として登録し、そのアドレスへの送信を停止してください。ユーザーにはメールアドレスを更新するよう依頼します。
  • complained — 受信者が迷惑メールとして報告しました。Portaly は、その後に送信する API メールをブロックしません。この状態を記録し、任意のメールもすべて送信しないようにしてください。
  • suppressed — Portaly の抑制リストに登録されているため、Portaly がこのアドレスへの送信をスキップしました(以前のハードバウンス、または複数の Portaly 送信者に対する迷惑メール報告が原因です)。抑制された受信者はクォータを消費しません。
  • soft_bounced — 再試行後も一時的なエラー(メールボックスがいっぱい、サーバーがダウンなど)が発生しています。同じアドレスで繰り返し発生しない限り、対応は不要です。
  • delivered は、受信側のサーバーがメールを受け付けたことを意味します。迷惑メールフォルダーに届いたかどうかは確認できません。
  • レスポンス内のアドレスはマスクされています(bu***@example.com)。受信者の順序は送信時と同じなので、順番に基づいて各ステータスをレコードに対応付けてください。

バウンスと迷惑メール報告は、作成者のドメインに対してカウントされます。バウンスが続くドメインは当日中ブロックされ(403 HARD_BOUNCE_LIMIT_REACHED)、その後停止されます。bounced への対応は必須です。

6. 安全にテストする
  • コードの動作確認:サンドボックスから、アカウント所有者自身のメールアドレスに送信します。

  • バウンスと迷惑メール報告の処理:検証済みのカスタムドメインから、メールボックスシミュレーターに送信します。これらのメールは実際の受信トレイには届かず、ドメインのバウンス率や迷惑メール報告率にもカウントされません(クォータは消費します)。

    アドレス結果
    [email protected]delivered
    [email protected]bounced
    [email protected]delivered の後、complained

    次に GET /api/email/emails/{id} をポーリングし、手順 5 の説明どおりに、ユーザーのコードがそのアドレスを処理することを確認します。

  • 実在するプロバイダーで、でたらめなアドレスを使ってテストしないでください([email protected]、[email protected] など)。実際のハードバウンスはカウントされ、数件発生しただけでも作成者のドメインが停止されることがあります。

ガードレール

  • この API を使ってマーケティングメールや一斉メールを送信しないでください — ニュースレター、プロモーション、コールドアウトリーチ、インポートしたリストの送信には、同意と配信停止を処理する Portaly 管理画面の一斉配信ツールを使用してください。ここから送信すると苦情につながり、クリエイターのドメインが停止され、領収書も一斉配信も送れなくなります。
  • 送信対象は、メールが届くことを想定している人に限ってください — ユーザー自身の顧客に、その人が行った操作をきっかけとして送信します。
  • メールを送信する公開フォームには、アドレスと IP ごとにレート制限を設けてください(パスワードリセット、サインアップ、お問い合わせ)。誰でもこれらのフォームを利用でき、短時間に大量の送信があると、クォータを消費し、苦情を招き、ドメインが停止されます。
  • キーをクライアント側に絶対に公開しないでください。ログに記録せず、コミットもしないでください。
  • SEND_QUOTA_EXCEEDED やその他の 4xx をループ内で再試行しないでください — 409 は上限までにしてください(ステップ 4)。Retry-After を尊重し、必ず同じ Idempotency-Key で再試行してください。
  • テストに架空の宛先アドレスを絶対に使わないでください — サンドボックスまたはシミュレーターを使用してください。
  • 開発中に実在の人へ実際のメールを送信する前に、必ず確認してください(アカウント所有者またはシミュレーター宛て以外)。送信先と内容を伝え、ユーザーの承諾を待ってください。
  • フィールド、ステータス、制限を推測して追加しないでください。ここに記載されている内容と https://portaly.ai/docs が食い違う場合は、ドキュメントを優先してください。
  • キーごとの 1 分あたりのレート制限:POST /emails は宛先 60 件、バッチ 10 件、読み取り 120 件、キャンセルまたはドメインのバインドは 20 件です。一度に集中させず、時間を分散して一括処理してください。

成果物

  • 人間向けの簡単なセットアップチェックリスト(キー、ドメイン、DNS、.env)
  • 冪等性と上記のエラー処理を備えた、プロジェクトの技術スタックに対応するサーバー側の sendEmail ヘルパー
  • bounced / complained のアドレスを記録する配信追跡ジョブ
  • サンドボックスとメールボックスシミュレーターを使ったテスト手順

リソース

  • references/api-contract.md — すべてのエンドポイント、フィールド、ステータス、エラーコード。
  • https://portaly.ai/docs(Email セクション)および https://portaly.ai/openapi.json — 正式な仕様。
  • ../portaly-overview/SKILL.md — 同じアプリでの決済など、Portaly でできるその他のこと。

コメント