SendLetter について
SendLetterは、Node向けのサービスとライブラリで、対応する欧州の宛先へ印刷した手紙を郵送できます。住所確認、見積もり、状況追跡に加え、冪等キーとテストモードで送信を管理できます。
主な機能
- 01印刷した手紙を郵送
- 02住所確認と料金見積もり
- 03状況追跡と手紙PDFの取得
- 04可能な場合に手紙をキャンセル
- 05請求書とクレジットノートの取得
- 06費用なしで送信フローをテスト
活用シーン
- 01請求書や督促状を郵送
- 02送信前に住所と郵送料を確認
- 03郵送状況の追跡と発送証明の取得
- 04本番利用前に郵送ワークフローをテスト
MCP 対応
入力にはSendLetterのライブラリとAPIが記載されていますが、MCP機能についての説明はありません。
設定
リモートサーバーはインストール不要です。クライアントを選んで設定をコピーするか、ワンクリックで導入できます。
https://sendletter.eu/api/mcpclaude mcp add --transport http eu-sendletter-sendletter https://sendletter.eu/api/mcp{"mcpServers":{"eu-sendletter-sendletter":{"type":"http","url":"https://sendletter.eu/api/mcp"}}}Cursor で開く{"servers":{"eu-sendletter-sendletter":{"type":"http","url":"https://sendletter.eu/api/mcp"}}}VS Code で開く{"mcpServers":{"eu-sendletter-sendletter":{"type":"http","url":"https://sendletter.eu/api/mcp"}}}{"mcpServers":{"eu-sendletter-sendletter":{"serverUrl":"https://sendletter.eu/api/mcp"}}}コマンドはターミナルで実行し、JSON は各クライアントの MCP 設定に貼り付けて再起動します。認可が必要なサーバーは初回接続時にログインを求めます。
ツール
詳細
sendletter
Node から本物の手紙を送ります。印刷、折りたたみ、切手の貼付を行い、郵便局に差し出します。
npm install sendletter
import { SendLetter } from 'sendletter'
const sendletter = new SendLetter(process.env.SENDLETTER_API_KEY!)
const input = {
idempotencyKey: 'invoice-2026-0042-reminder-1',
sender: {
company: 'Twilper',
name: 'Gert Snijder',
street: 'Merelstraat',
number: '64',
postalCode: '8916 AX',
city: 'Leeuwarden',
country: 'NL',
},
recipient: {
name: 'Jan de Vries',
street: 'Keizersgracht',
number: '123',
postalCode: '1015 CJ',
city: 'Amsterdam',
country: 'NL',
},
text: 'Beste heer De Vries,\n\nBijgaand de herinnering voor factuur 2026-0042.',
}
const prepared = await sendletter.prepare(input)
console.log(prepared.id, prepared.pages, prepared.totalCents)
const pdf = await sendletter.download(prepared.id)
PDF と正確な合計金額をユーザーに表示します。承認されたら、同じ入力とキーを使います。
const letter = await sendletter.send({ ...input, expectedTotalCents: prepared.totalCents })
console.log(letter.id, letter.status, letter.totalCents)
キーは sendletter.eu で取得できます。sk_test_ で始まるキーでは、郵送も課金もされませんが、ステータスの一連の流れはすべて実行されるため、実環境と同じ条件で開発できます。
送信
内容は text、document(リッチテキスト)、file(base64)、fileUrl のいずれか一つで指定してください。二つ指定すると、どちらを使うか推測するのではなく拒否されます。間違った書類を郵便ポストに投函してしまうと、取り戻せないためです。
// An invoice you already have as a PDF.
await sendletter.send({
sender,
recipient,
file: { name: 'invoice.pdf', contentBase64: pdf.toString('base64') },
product: 'registered', // 'standard' | 'priority' | 'registered'
colour: true,
idempotencyKey: `invoice-${invoice.id}`,
})
再試行の可能性がある場合は、必ず idempotencyKey を指定してください。 同じキーで再度送信すると、二通目の封筒ではなく、元の手紙が返されます。このフィールドがあれば、ネットワークのタイムアウト後も安全に再試行できます。タイムアウトしても、手紙が受け付けられたかどうかは分かりません。
読み取り
await sendletter.get(id) // one letter
await sendletter.list({ status: 'posted' }) // one page
await sendletter.cancel(id, 'order withdrawn') // while it is still cancellable
await sendletter.download(id) // the PDF as printed
await sendletter.download(id, { proof: true }) // proof of posting
for await (const letter of sendletter.all({ mode: 'live' })) {
// walks every page; you never hold the cursor
}
請求書とクレジットノート
有料の本番環境の手紙には、支払い時に請求書番号が付与されます。テストの手紙では法定の連番は消費されません。全額返金の場合も請求書番号はそのまま残り、別の番号が付いたマイナスのクレジットノートが追加されます。
const page = await sendletter.listInvoices()
await writeFile('invoice.pdf', await sendletter.downloadInvoice(page.data[0].id))
if (page.data[0].creditNote) {
await writeFile(
'credit-note.pdf',
await sendletter.downloadCreditNote(page.data[0].creditNote.id),
)
}
料金をかける前の確認
const check = await sendletter.validateAddress(recipient)
if (!check.valid) console.log(check.problems) // answers, does not throw
const quote = await sendletter.quote({ destination: 'DE', pages: 3 })
validateAddress は不正な住所を例外としてスローせずに返します。誤った郵便番号が返るのは、問い合わせが正常に行われた結果です。住所を編集しても修正できないのは supported: false の場合です。その国への郵送には対応していません。
エラー
API によって拒否された内容はすべて、分岐に使える code を持つ SendLetterError になります。
import { SendLetterError } from 'sendletter'
try {
await sendletter.send({ sender, recipient, text })
} catch (error) {
if (!(error instanceof SendLetterError)) throw error
if (error.code === 'insufficient_balance') {
// Put this in front of the customer; the wallet is short, nothing else.
return redirect(error.topUpUrl!)
}
if (error.isRetryable) {
await sleep((error.retryAfter ?? 5) * 1000)
}
}
isRetryable の対象は 429 と 5xx です。それ以外は再試行しないでください。400 の場合、二度目も同じように手紙が拒否されます。
Webhook
ステータスが変わると、登録した URL に POST で通知されます。必ず検証してください。 検証しない場合、エンドポイントを知った人なら誰でも、手紙が配達されたとシステムに通知できてしまいます。
import { verifyWebhook, SendLetterError } from 'sendletter'
export async function POST(request: Request) {
const rawBody = await request.text() // the raw text, not the parsed object
let event
try {
event = verifyWebhook({
rawBody,
signature: request.headers.get('x-sendletter-signature'),
secret: process.env.SENDLETTER_WEBHOOK_SECRET!,
})
} catch (error) {
if (error instanceof SendLetterError) return new Response('nope', { status: 400 })
throw error
}
if (await alreadyHandled(event.id)) return new Response('ok') // see below
await handle(event) // letter.posted, ...
return new Response('ok')
}
この形式には、手作業で検証する場合には見落としがちな、適切な点が二つあります。
- 再シリアライズした本文ではなく、生の本文を使います。 署名の対象は送信した正確なバイト列であり、
JSON.stringifyが同じ内容を再現する保証はありません。本文をテキストとして読み取り、検証してから解析してください。 event.idで重複を排除します。 配信は再試行を伴う少なくとも一回の方式なので、受信側でタイムアウトすると同じイベントが再び届きます。letter.postedを二度処理しても、顧客に二重請求しないようにしてください。
イベント:letter.submitted、letter.printed、letter.posted、letter.delivered、letter.failed、letter.refunded。
テストモード
sk_test_ キーを使うと、ウォレットにもプリンターにも影響を与えず、Webhook も本番環境とまったく同じように発火しながら、一連の流れ(submitted → printed → posted → delivered)全体が実行されます。client.isTestMode で使用中のキーの種類を確認できます。デプロイ時のチェックで必ず検証するとよいでしょう。ログ上では二つのキーは見分けがつかず、料金が発生するのは片方だけです。
リファレンス
sendletter.eu/en/developers · /api/v1/openapi.json に OpenAPI 仕様 · MIT
コメント
AI アプリケーションが外部のツールやデータソースに統一的な方法で接続するためのオープンなプロトコルです。クライアントはモデルとの対話を担い、MCP サーバーはツール・リソース・プロンプトテンプレートを提供します。標準メッセージでやり取りするため、1 つのサーバーを複数のクライアントで再利用できます。