Skip to main content
Drop one script on your page and any element becomes a “mail this” trigger. Two widgets, same modal pattern, no account or API key:
  • paperplane.js — the full send flow (compose or upload, address, pay). For “mail this document” on tools and dashboards.
  • template-embed.js — a single pre-filled letter template (deposit demand, cease & desist, late-rent notice, 50+ more). For content pages where the reader’s next step is one specific letter.
Full send flow
One specific template
Buttons carry their own template key, so one page can offer several letters. Every key in the template gallery works.

See it live, in context

Three fictional host sites with the real widget wired in — click the buttons, they work (the flow is the live guest checkout; nothing is mailed unless someone pays):

A news / advocacy article

Inline reader tool next to the story it serves — the highest-converting spot, because intent peaks mid-article.
Multiple templates, one script — each button carries its own data-paperplane-template key.

A small-business portal

Both widgets together: the generic send flow for arbitrary documents plus a pre-filled compliance template.

How it works / security

  • Each script is ~3kb, dependency-free; it renders a modal iframe of the no-chrome flow (/embed or /templates/embed/{key}), with its own 44px in-frame header (paperplane wordmark, “Printed & mailed by paperplane · USPS”, a close control) and a slim footer with Terms/Privacy links.
  • The app allows third-party framing only on those embed routes (Content-Security-Policy: frame-ancestors * scoped per-route) — nothing else on the app can be framed, so nothing else can be clickjacked.
  • Payment happens inside the frame. Card payment uses Stripe Embedded Checkout — Stripe’s hosted Checkout page refuses to load inside any iframe at all, so paperplane.js’s iframe needs allow="payment" (already set) for the in-frame card fields and wallet buttons to work. Apple Pay only appears for domains registered with Stripe — paperplane’s own domain is, an arbitrary third-party host is not, and no allow attribute changes that; Google Pay, Link, and card entry are unaffected.
  • Closing is never destructive. Backdrop click, the header’s × control, and Escape all go through the same confirmation: closing with a draft in progress shows “Discard this letter?” first, closing with nothing to lose (an untouched step 1) closes immediately, and none of them can close while a submission or payment is in flight. The modal is hidden (display:none), not unloaded — reopening within the same page load restores the draft instead of reloading /embed from scratch.
  • Prefill. A trigger button’s data-template, data-address, and data-class attributes are forwarded onto /embed’s query string when it opens — same ?template=/?address=/?class= the standalone /send page reads. All three are starting points a visitor can still change, not trusted input.
  • Events. window.paperplane.on('opened' | 'closed' | 'ordered', fn)'ordered' fires once per completed order with { orderId, amountCents, mailClass }. The same three also dispatch as document-level CustomEvents (paperplane:opened etc.) if you’d rather addEventListener.
  • Template embeds show “Powered by your site & paperplane” from the referrer (best-effort; plain paperplane branding when the referrer is blocked).
  • Visitors stay guests: address verification, transparent pricing, payment, and a private tracking link — all inside the modal. Because third-party iframe storage is partitioned per top-level site, the full send flow requires an email in the embed context (it’s the only way to find the order again later), and “Track your letter” opens in the top-level page rather than the iframe.
  • Co-branding. Both widgets are subtly co-branded: a small “Powered by paperplane” line (with the mark) sits at the bottom of the full send flow, and the template widget’s “Powered by your site & paperplane” line links back to paperplane the same way. Neither is optional on the default plan. A 100% whitelabel option — the paperplane mention removed entirely — is available; contact sales to turn it on for your embed.
  • The address field’s “Use my location” button needs the iframe to grant geolocation — paperplane.js and template-embed.js already set allow="geolocation; payment" (template-embed.js omits payment, since it never reaches a card) on the iframe they create. If you build your own iframe wrapper around /embed or /templates/embed/{key} instead of using the provided script, add the same allow values yourself or those features will silently fail (the browser blocks geolocation and payment in a cross-origin iframe by default).

Where hosts use it

  1. Publishers & advocacy — the reader tool beside the article (demo above).
  2. Legal & professional services — self-help letter libraries; certified mail is the standard courts expect.
  3. SMB portals — property management, billing, back-office “mail this notice” buttons that replace the post-office run.

Limits

The embedded flow inherits the same guest limits as the web surface (see Rate limits); no separate quota, no host-site keys to rotate.
Last modified on September 17, 2026