The invoice generator
While you are filling in the form, nothing leaves your browser. The invoice is held in browser storage under the key paperpony.invoice-generator.v1 so a reload does not lose your work, and the logo you upload is read locally and never uploaded on its own. Clearing site data removes both. There is no account, so there is nothing for any of it to be attached to.
When you press download, the invoice is posted once to a Cloudflare Worker of ours, which passes it to the renderer. The renderer produces the PDF and puts it in a private bucket behind a signed link. That stored copy lives for one hour, and a sweep that runs every fifteen minutes deletes it from the bucket and then clears the row that pointed at it.
One hour rather than the thirty days the underlying plan allows, because the documents are yours and the purpose ends when your browser has the file.
What the row keeps after that is the fact of the render and not the invoice: status, size, page count, timings, and the page settings we rendered with. What it stops keeping, in the same statement that deletes the file, is the filename and the running header and footer, because those carried your invoice number. A render that failed has no file to delete, so the same clearing runs against it on the same hour by a different path.
Downloads are limited to ten an hour from one connection so the tool stays available. Cloudflare gives the Worker your address, the Worker turns it into an identifier for a counter, and the counter holds a list of timestamps. The address itself is not written anywhere.
Asking for an API key
The form on the key page stores four things: the email address you type, the sentence about what you are building, the address the request came from, and the time. They go into a table in our database and are also emailed to us. The free-text field is never written to a log.
The address is kept for one reason, which is telling a person apart from a script. If you want the row deleted, write to hello@paperpony.dev and it is deleted.
Two messages are sent about a request: one to us saying it arrived, and one to you carrying your key. For each we keep the address it went to, which kind of message it was, when it was sent, and what our mail provider said afterwards, including the reason if it bounced. The messages themselves are not stored. The one with your key in it exists in your inbox and nowhere else here, which is why a lost key is replaced rather than resent.
We do not know whether you opened it. Delivery tracking of that kind is a hidden image in the message, and there is none: what we learn is only that something failed, and only because a failure is reported back to us.
A request is kept for twelve months and then deleted, whether or not it was answered. The same sweep that clears expired files does it, on the same schedule, so the period is something that happens rather than something written down here.
Signing in
There are no passwords. You type an address on app.paperpony.dev, we send a link, and following it signs you in. So there is no password stored, none to reset, and nothing of that kind to leak.
Asking for a link writes a row holding the address, a keyed hash of the link's token, the address the request came from, when it expires and when it was used. The token itself is never stored, so the message in your inbox is the only copy, which is why a link that has been used cannot be reissued. The row is deleted a day after it expires, by the same sweep that clears everything else.
The form answers the same page whatever you type. A known address, an unknown one, one that is not an address at all: all three look identical from outside, because a form that answered differently would be a way to ask us who our customers are.
Being signed in is a row and a cookie. The row holds a keyed hash of what is in the cookie, when the session was made, when it was last used and when it expires, which is thirty days. The cookie holds a random string and nothing else: no address, no identifier, nothing readable. It is HttpOnly, sent only over HTTPS, and scoped to app.paperpony.dev, so it is never sent to this site or to the API. Signing out marks that row and leaves your other browsers alone.
The message itself is recorded the way every message is: which kind it was, the address it went to, when, and what our mail provider said afterwards. Not the body, which is the link.
If you have an API key
The account row holds your email address, a name, the plan and the credit counters. Keys are stored as an HMAC and a twelve-character prefix, never as the key itself, which is why a lost key is replaced rather than recovered.
The account and the person signing in are separate rows. The account is the customer, which is usually a company and carries the address billing goes to; the person is whoever signs in, which is usually a developer with an address of their own. Today one person belongs to one account. Nothing about that is visible yet and it exists so that adding a colleague later does not mean rebuilding the model.
Each render writes a job row. That row is metadata about the render and not the document: status, byte size, page count, credits charged, the timings, the object key, and the options you sent. The HTML you sent and the data you sent are not stored. A stored template is the exception and is obvious: its source is the thing you asked us to keep, so we keep it until you archive it.
Three of those options are your text rather than a setting: the filename, and the header and footer templates that print on every page. They are cleared when your output is deleted, in the same statement, so they live exactly as long as the file did and not a sweep longer. The rest of the options describe how we rendered rather than what, and those stay.
One place holds the document itself, briefly. A render is queued in Redis with the payload attached; a job that finishes drops it immediately, and a job that fails keeps it for fifteen minutes so the failure can be looked at, then it goes too.
Rendered files live for the retention on your plan, which is 24 hours on Free, seven days on Starter and thirty on Growth and Scale. The same sweep deletes them.
What the logs contain
One line per request, carrying the request id, the account id, the route, the method, the status code, the duration and whether it succeeded. No query string, no headers, no body.
That is the application's log. The web server in front of the API keeps its own, which is the ordinary one every web server keeps: the calling IP address, the request line, the response size, the referrer and the user agent. It is rotated daily and fourteen days are kept. Naming it here because the paragraph above would otherwise read as the whole account of what is written down, and it is not.
On top of that the logger is configured to blank out a list of field names wherever they appear, including authorization, data, html, source, key, secret and password, and to rewrite anything shaped like an API key. A rendered document has never been in a log line and the redaction is the second lock rather than the first.
If you pay us
Stripe takes the payment and we never see the card. Choosing a plan sends you to a page on Stripe’s own domain, and the card number, the expiry and the security code are typed there and go to them. Nothing about a card reaches this system, and there is no field for one anywhere in it.
Stripe asks for a billing address, and a VAT registration number if you have one, because the tax on the invoice depends on both. They compute it, they issue the invoice and they keep it.
What our database holds is identifiers and states: Stripe’s customer id, the subscription id, which plan it is for, whether the last payment succeeded, and for each invoice its id, its status, the period it covers and the link to it on Stripe. No amount, no tax figure, no address, no name from the billing form. Deliberately: Stripe computed those and a copy here would be a second version of a document you already hold.
Cancelling ends the subscription at the end of the period you have paid for and puts the account on the free plan. Nothing is deleted at that point, or at any other: your keys, templates and render history stay where they are.
Who else touches it
Cloudflare serves this site, runs the Worker behind the invoice generator, and stores both the rendered objects and the daily database backup. Hetzner runs the machine with the API, the renderer and the database. Zoho carries our mail, through ZeptoMail for anything the system sends you and through Zoho Mail for the mailbox a person reads. Stripe processes payments and holds the billing details described above. That is the whole list.
The database is dumped once a day and a dump is kept for thirty days, in the same EU bucket, read only to restore. So a row deleted from the live database, by your request or by one of the sweeps above, can still exist in a backup for up to thirty days after it goes. Saying so because the word “deleted” on this page otherwise implies something faster than the truth.
There is no analytics script on this site. The code can load Cloudflare Web Analytics if a token is configured, and no token is configured, so nothing is loaded. The fonts are served from our own origin rather than from a font CDN, which is the same decision made for the same reason.
Visiting this site sets no cookies, from us or from anyone else, and neither does using the invoice generator or the API. Two cookies exist in the whole system and both are sign-in cookies on other hostnames. One is on app.paperpony.dev for customers, described above. The other is on api.paperpony.devfor the person who runs the service. Neither is ever sent to this site and neither is reachable from it. Saying so because “no cookies” would otherwise be a sentence that is true of what you experience and false about the system, and you should not have to work that out.
Error reporting to Sentry is wired into the API and the renderer and is switched off: with no DSN set the client is never started. If it is ever turned on, the request body, headers, cookies and query string are stripped before anything is sent.
What we do not do
We do not sell data, share it with advertisers, or use anything you send to train a model of any kind. Your documents are rendered and then deleted on the schedule above. There is no secondary use.
Who holds it
YTI Digital OÜ, registered in Estonia under number 17298015, VAT EE102925876, in Tallinn. The R2 bucket holding rendered files is set to the EU jurisdiction, so objects stay in the European Union.
The API, the renderer and the database run on one Hetzner machine in Nuremberg, Germany. Nothing of yours is processed outside the European Union.
Deletion and questions
Write to hello@paperpony.dev. That reaches a person, and it is the address for a deletion request, a copy of what is held, or a question about any of the above.
Closing an account is done by asking rather than by a button. There is no self-service delete: a request goes to that address and a person does it, which for something irreversible is the arrangement we would want on the other side of.