REST API
API Documentation (v1, legacy)
Authentication and a first request
All requests go over HTTPS to https://api.printfriendly.com and use HTTP Basic authentication: the API key is the username, and no password is needed. Your key is on your API key page.
The other ways to send a key (the api_key query parameter or body field, Authorization: Bearer, and X-API-Key) work on v1 as well, and are described in the v2 authentication section. On v1 the choice never changes the reply.
curl -i -u YOUR_API_KEY: https://api.printfriendly.com/v1/pdfs/create \
-d page_url=https://en.wikipedia.org/wiki/Eclipse| Flag | Meaning |
|---|---|
| -u | Basic credentials. The colon after the key stops curl asking for a password. |
| -d | Send the parameters in a POST body. |
| -i | Show the response headers, so you can see the redirect. |
Endpoints
| Endpoint | Input | Output |
|---|---|---|
| /v1/pdfs/create | page_url or html | A PDF file. Also available as /v1/pdf/create. |
| /v1/html/create | page_url or html | A standalone, printable HTML document. |
There is no plain-text endpoint on v1; use /v2/text/create. POST is the usual choice, and GET works as well.
The response: always a redirect
A successful v1 request always answers 302 Found, whichever input you sent (a page_url or html) and whichever endpoint you called. The Location header is the file's address, and the body repeats it:
HTTP/1.1 302 Found
Location: https://storage.printfriendly.com/data-production/pdf_1531957353_e94df5Letter.pdf
{"success":"Redirect to https://storage.printfriendly.com/data-production/pdf_1531957353_e94df5Letter.pdf"}Some HTTP clients follow the redirect on their own and hand you the file (curl does so only with -L). To get just the address, read the Location header with redirect-following turned off.
Parameters
Send page_url or html. Everything else is optional. These are the same parameters as v2, except output_type, which v1 ignores.
| Parameter | Default | What it does |
|---|---|---|
| page_url | none | Address of a public web page (http or https). We fetch it, pull out the article and render it. Send this or html. Addresses ending in .pdf are rejected. |
| html | none | Your own HTML, rendered as it is with no article extraction. Up to 50 MB. Send this or page_url; if you send both, page_url wins. |
| title | Print Friendly | Document title, used for the PDF title and the file name. With page_url the page's own title is used and this is only the fallback. |
| page_size | Letter | PDF page size: A4 or Letter. Any other value gives Letter. Has no effect on html or text output. |
| css_url | none | Address of a stylesheet to apply to the PDF or HTML output. Alias: custom_css_url. Ignored by text/create. |
| custom_css_url | none | Alias for css_url. If both are sent, css_url is used. |
| header_image_url | none | Address of an image to show in the document header. Only used with page_url, and only for PDF and HTML output. |
| header_text | none | A line of text (for example a tagline) shown in the document header. Only used with page_url, and only for PDF and HTML output. |
| dir | none | Text direction, ltr or rtl. With page_url the direction we detect on the page wins and this is the fallback. |
| lang | none | Language code for the document, for example en or ar. Sets the lang attribute on the output. |
| hostname | host of page_url | Host name used when naming the output file. Only changes the file name. |
| api_key | none | Your API key, as a query parameter or a body field. Prefer the Authorization header. |
Errors
A failed request returns a JSON body. Every failure carries error.code, a stable snake_case name, and error.retryable, which is true when sending the same request again can succeed. They sit alongside the fields older integrations already read, which have not changed. Branch on the code and the flag, not on the English text, which can be reworded.
Fields
| Field | Present on | Meaning |
|---|---|---|
| error.code | every failure | Stable name for what went wrong. See the table below. |
| error.message | every failure | Plain English description. Read this one for display; it exists on every failure. |
| error.retryable | every failure | true if the same request can succeed later. |
| error.type, error.errors | 400 and 401 | type is always "parameters-validation". errors maps each field name to a list of problems. |
| status | 402, 422 and 429 (not on 400 or 401) | Always "failed". |
| message | 402, 422 and 429 | The same text as error.message, at the top level. |
| error.upgrade_url | 402 | Where to upgrade. |
| error.retry_after_seconds | 429 | Seconds to wait. Also sent as the Retry-After header. |
HTTP status codes
| Status | Meaning |
|---|---|
| 200 | The document was created. Read file_url (or the file itself). |
| 202 | An async /v2/convert job was accepted. Read it later at status_url. |
| 400 | A parameter is missing or invalid. |
| 401 | The key is missing, unknown, inactive or at a limit set on it. |
| 402 | The free plan's monthly documents are used up. |
| 404 | No job with that id for this key (GET /v2/jobs/{id}). |
| 422 | The request was valid but we could not produce the document. |
| 429 | Too many requests this minute. See Retry-After. |
Error codes
| error.code | HTTP | Retry? | Meaning |
|---|---|---|---|
| invalid_parameters | 400 | no | A parameter is missing or invalid. error.errors names each field and what is wrong with it. |
| api_key_required | 401 | no | No API key was sent. |
| invalid_api_key | 401 | no | The key does not exist. Check for a typo, or whether you regenerated the key. |
| subscription_inactive | 401 | no | The plan on this key was canceled or has expired. |
| limit_exceeded | 401 | no | A usage limit set on this key was reached. Contact support to raise it. |
| quota_exceeded | 402 | no | The plan's documents are used up. On the free plan that is 100 a month, and the count resets on the 1st (UTC). On a paid plan it is the quota with overage off, or the monthly overage cap, until the next billing period. error.upgrade_url says where a person can upgrade or change the overage setting. |
| rate_limited | 429 | yes | Too many requests in a minute. Wait the number of seconds in the Retry-After header (also error.retry_after_seconds), then try again. |
| fetch_failed | 422 | yes | We could not load page_url: the connection timed out or was refused, or the site blocks us. retryable is false when the site is blocking us. |
| parse_failed | 422 | no | We loaded the address but found no article content, or it is not a web page (a PDF, a text file or an image). |
| homepage_url | 422 | no | page_url is a site's home page, which has no single article to extract. Link to a specific page. |
| content_blocked | 422 | no | Returned only when content checks are enabled for the key's plan. The page failed the same content checks printfriendly.com applies under the Acceptable Use Policy, so no document was made. The same page fails every time, so do not retry it. |
| render_timeout | 422 | yes | The document was not ready inside 27 seconds, or the service was too busy to start it. |
| render_failed | 422 | yes | Something unexpected went wrong while producing the document. Safe to try again. |
| html_too_large | 400 or 422 | no | 400: the html parameter is over 50 MB. 422: the page at page_url is too large for us to process. |
| job_not_found | 404 | no | GET /v2/jobs/{id} found no job for that id under this key. The id may be wrong, the job may belong to another key, or it may be older than 48 hours. |
Internal details such as fetch-error or high-load are not part of the contract. They are folded into the codes above, and the English sentence that goes with them is unchanged.
Examples
A missing parameter (400):
{
"error": {
"type": "parameters-validation",
"errors": { "base": ["You must provide a page_url or html parameter"] },
"message": "You must provide a page_url or html parameter",
"code": "invalid_parameters",
"retryable": false
}
}A page we could not load (422):
{
"status": "failed",
"message": "Print technology timed-out while trying to connect to this page",
"error": {
"code": "fetch_failed",
"message": "Print technology timed-out while trying to connect to this page",
"retryable": true
}
}The free plan used up for the month (402). A paid plan stopped by its overage setting answers the same way, with its own message and a link to the API key page:
{
"status": "failed",
"message": "Your free plan includes 100 documents a month. Upgrade for more.",
"error": {
"code": "quota_exceeded",
"message": "Your free plan includes 100 documents a month. Upgrade for more.",
"upgrade_url": "https://www.printfriendly.com/api#pricing",
"retryable": false
}
}Too many requests this minute (429):
HTTP/1.1 429 Too Many Requests
Retry-After: 23
{
"status": "failed",
"message": "Too many requests. This key allows 10 requests per minute. Retry in 23 seconds.",
"error": {
"code": "rate_limited",
"message": "Too many requests. This key allows 10 requests per minute. Retry in 23 seconds.",
"retryable": true,
"retry_after_seconds": 23
}
}Limits and quotas
Free plan
- Every account can create a free key on its API key page. No credit card is needed.
- It includes 100 documents per calendar month (UTC). The count resets on the 1st.
- It is a hard cap. Past it, requests answer
402withquota_exceededuntil the month turns over or you upgrade. - PDFs made with a free key carry the line “Made with the PrintFriendly API (printfriendly.com/api)” at the foot of every page, and HTML carries it once at the end. Markdown, JSON, text and Word files have none, and paid plans never do.
- When you subscribe to a paid plan the same key keeps working. It just stops being a free key.
- It works the other way too. You can downgrade to the free plan from your API key page at any time: the paid plan runs to the end of the period you have paid for, then the same key becomes a free key again. A paid plan that ends for any other reason lands there as well.
Paid plans
- Each plan has a monthly quota of PDF, HTML and text documents. Usage resets at the start of every billing period.
- What happens past the quota is up to you, with the overage switch on your API key page. With overage on, documents past your quota keep working and are billed at your plan's overage rate on your next invoice. See API pricing for rates. With overage off, requests stop at the quota.
- With overage on you can also set a monthly cap in dollars. Requests stop once the extra documents reach it.
- A stopped request answers
402withquota_exceededuntil the next billing period starts, or until you turn on overage, raise the cap or upgrade.error.upgrade_urllinks to your API key page. - A new subscription starts with overage off, so it never bills past its quota until you turn overage on.
- We email you at 80% and 100% of your quota, and when your cap is reached, once each per billing period.
- If your plan ends while overage is on, extra documents from its last period are billed on a closing invoice.
- You can see your usage for the current period on your API key page.
What counts
- Only successful documents count. Requests that fail (an invalid key, invalid parameters, or a page we can't reach or render) are never counted, and neither are requests refused for rate limiting.
- If a request times out, the document may still finish on our side, and it counts if it does.
Rate limits, time and size
- Requests per minute, per key: 10 on the free plan, 120 on paid plans. Over the limit you get
429with aRetry-Afterheader. - Requests are synchronous with a 27 second ceiling. A document that is not ready by then answers
422withrender_timeout. - The html parameter can be up to 50 MB.
Use of the API is governed by the Acceptable Use Policy, and keys used in breach of it are revoked.