关于 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
状态变更会以 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_ 密钥会运行完整链路——submitted → printed → posted → delivered——并触发与生产环境完全相同的 Webhook,但不会动用钱包或打印机。client.isTestMode 会告诉你当前使用的是哪种密钥;最好在部署检查中断言这一点:它们在日志中看起来一模一样,但只有一种会产生费用。
参考
sendletter.eu/en/developers ·
OpenAPI 位于 /api/v1/openapi.json · MIT
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。