SendLetter

eu.sendletter由 sendletter.eu 发布官方发布者v1.0.0更新于 Oct 24, 2026

Send real, printed letters by post across Europe. Requires a free account; quote before sending.

已验证Streamable HTTP可网页运行效率与工作流商业与电商

概览

AI 生成的概览

SendLetter 让助手在欧洲范围内准备、报价并寄出真实的纸质信件,并跟踪信件状态与发票。

可以做什么
用文本、富文本、base64 文件或文件链接准备信件,寄出前返回 PDF 预览和准确总价,可选标准、优先或挂号投递以及彩色打印。校验收件地址,并按目的地和页数获取报价。寄出、列出、查询、取消和下载信件,包括投递证明。列出并下载发票与贷项通知单。接收状态变更 webhook,涵盖提交、打印、投递、送达、失败和退款。
适用场景
适合让助手把文档或消息变成在欧洲邮寄的实体信件,例如发票提醒或正式函件,并先展示预览和价格供确认。测试密钥可在不打印、不扣费的情况下跑通完整流程。
运行要求
位于 sendletter.eu 的远程端点,使用 OAuth 登录,并需要一个免费的 SendLetter 账户。寄信需要 API 密钥,README 中以 SENDLETTER_API_KEY 传入;验证 webhook 需要签名密钥,以 SENDLETTER_WEBHOOK_SECRET 表示。以 sk_test_ 开头的密钥为测试模式。
安装前请注意
正式信件会产生费用并从钱包余额扣款,寄出前应与用户确认 PDF 和准确总价。信件一旦寄出无法撤回,可重试的操作应带上幂等键。内容只能以一种方式提供。webhook 需按原始请求体验证签名并按事件 id 去重,否则重复事件可能导致重复向客户收费。寄件人和收件人的邮寄地址会发送给 SendLetter。

安装

在 SourceWeft 中

  1. 打开 控制台中的 SendLetter,将其添加到工作区。
  2. 连接 所需的 凭据。它们会被私密存储,绝不会在公开页面上展示。
  3. 为需要使用其工具的对话启用该服务。

Web executable,通过 Streamable HTTP。 远程服务在工作区中配置后即可从网页运行时运行。

交给智能体安装

把这段提示词粘贴到 Claude Code、Codex、Cursor 等智能体,或 SourceWeft 对话中。智能体会读取该服务器的安装指南,先向你展示发布者、端点和登录方式,经你同意后再把它添加到你的客户端。密钥不会写进任何命令。

阅读 https://sourceweft.com/mcp/eu.sendletter%2Fsendletter/install.md,并按其中说明把这个 MCP 服务器接入我的客户端。修改任何配置之前,先告诉我它的发布者、端点和登录方式,等我确认。不要把 API 密钥写进命令或我会提交的文件;问我密钥应从哪里获取。

查看智能体所依据的安装指南

其他 MCP 客户端

把它添加到你客户端的 mcpServers 配置中。 客户端会在首次连接时完成 OAuth 登录。

{
  "mcpServers": {
    "sendletter": {
      "type": "http",
      "url": "https://sendletter.eu/api/mcp"
    }
  }
}

README

sendletter

Send a real letter — printed, folded, franked and handed to the post — from Node.

bash
npm install sendletter
ts
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)

Show the PDF and exact total to the user. After approval, use the same input and key:

ts
const letter = await sendletter.send({ ...input, expectedTotalCents: prepared.totalCents })
console.log(letter.id, letter.status, letter.totalCents)

Get a key at sendletter.eu. Keys starting sk_test_ post nothing, charge nothing, and still run the whole status chain, so you can build against the real thing.

Sending

Give the content exactly one way: text, document (rich text), file (base64) or fileUrl. Two is refused rather than guessed at, because the wrong document in a postbox cannot be recalled.

ts
// 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}`,})

Send an idempotencyKey on anything that can be retried. A repeat with the same key returns the original letter instead of a second envelope. That single field is what makes a retry after a network timeout safe, and a timeout says nothing about whether the letter was accepted.

Reading

ts
await sendletter.get(id)                       // one letterawait sendletter.list({ status: 'posted' })    // one pageawait sendletter.cancel(id, 'order withdrawn') // while it is still cancellableawait sendletter.download(id)                  // the PDF as printedawait 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}

Invoices and credit notes

Every paid live letter receives its invoice number at payment time. Test letters never consume the statutory series. A full refund keeps that invoice and adds a separately numbered negative credit note.

ts
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),  )}

Checking before you spend

ts
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 returns a bad address rather than throwing: a wrong postcode is the successful outcome of asking. supported: false is the one that cannot be fixed by editing the address — we do not carry to that country.

Errors

Everything the API refuses becomes a SendLetterError with a code to branch on.

ts
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 covers 429 and 5xx. Nothing else should be retried: a 400 means the letter will be refused just as firmly the second time.

Webhooks

Status changes arrive as a POST to the URL you registered. Verify them. Without that, anyone who learns your endpoint can tell your system a letter was delivered.

ts
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')}

Two things this shape gets right and hand-rolled verification usually does not:

  • The raw body, not a re-serialised one. The signature covers the exact bytes we sent, and JSON.stringify does not promise to reproduce them. Read the body as text, verify, then parse.
  • Deduplicate on event.id. Delivery is at-least-once with retries, so a timeout on your side means the same event arrives again. letter.posted handled twice should not bill a customer twice.

Events: letter.submitted, letter.printed, letter.posted, letter.delivered, letter.failed, letter.refunded.

Test mode

A sk_test_ key runs the full chain — submitted → printed → posted → delivered — with webhooks firing exactly as they will in production, while touching no wallet and no printer. client.isTestMode tells you which kind of key you are holding, which is worth asserting in a deploy check: the two look alike in a log and only one of them costs money.

Reference

sendletter.eu/en/developers · OpenAPI at /api/v1/openapi.json · MIT

来源:README.md,提交 fca2556

工具

0
该服务器需要登录,因此 SourceWeft 无法在没有账号的情况下读取其工具,也未收录。已连接账号所能看到的内容可能因账号及其权限而异。

版本历史

1
  1. v1.0.0最新Sep 16, 2026

评分

已满足适用检查项的 85%

该评分衡量此收录条目的完整程度,依据注册中心条目、端点与公开来源计算得出。它不是对服务器本身的评价,也不表示服务器安全或当前可用:可连通性是带日期的检查结果,评分也不包含安装量或用户评价。

  • 近 30 天内可连通必需25 分
  • 已声明认证方式必需10 分
  • 已列出工具15 分

    请让服务器在无凭据时响应 tools/list,或发布可在沙箱中无凭据启动的软件包。

  • 描述必需15 分
  • 图标10 分
  • 文档15 分
  • 许可证10 分