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 分