Every render call needs your API key. The usual place is a header:
Authorization: Bearer <your api key>
Keys come from the dashboard at dashboard.ireceipt.pro. The rest of this page is about the failure cases, because they are less obvious than the success case.
If you are looking at a 401 or a 403 right now, the short version is on errors and retries: 401 means no key reached the server, 403 means one did and was not accepted. This page explains what counts as "reached" and what else a 403 can mean.
Two ways in, and two that look like ways in
The server reads the key from the Authorization header or,
if there is no such header, from a token query parameter.
The API reference lists token as the alternative to the
header, and it is what makes a plain GET link possible. A
key in a URL ends up in server logs and browser history, so keep it for
cases where a header is impossible.
X-Api-Key and ?api_key= are common in this
category, and neither is read. Neither produces an error saying so:
they come back as 401, the same response as sending
nothing, because from the server's point of view that is what you did.
# read, and rejected: 403 means the server saw a key
curl -X POST '...?token=not-a-real-key' # 403 {"message":"Token invalid"}
# not read at all: 401, same as no credential
curl -X POST '...?api_key=<key>' # 401 {"message":"Unauthorized"}
curl -X POST ... -H 'X-Api-Key: <key>' # 401 {"message":"Unauthorized"}
So a 401 while you are holding a key you are certain is good is almost always a question of where you put it.
Public templates need a key too
Worth saying because the URL suggests otherwise. The path is:
POST https://api.ireceipt.pro/v1/{format}/{scope}/{templateId}
{scope} is public or private, and
it describes the template — whether it is one of the shared
library templates or one you authored in your own dashboard. It does not
describe the endpoint. A call to a public template with no key gets a 401
exactly like a private one.
What the server does with the header
It removes the word bearer, in any case, and all
whitespace, and looks up what is left as a key. Which means:
-
Bearer <key>,bearer <key>and the bare<key>all work. The prefix is conventional, not required. -
Basic …does not: the wordBasicstays glued to the key, and the result resolves to nothing — 403. -
A header with an empty value reads as absent — 401. A header
of
Bearerwith nothing after it reads as present — 403. If your code builds the header by concatenation and the key variable came back undefined, that is the difference you are seeing.
With a key that does not exist, every variant answers
403 {"message":"Token invalid"}. "Token invalid" means
exactly that: what was left after the clean-up is not a live key of any
account.
Which check answers first
A call goes through four gates in a fixed order, and the first one that fails answers. Knowing the order tells you what a status code can and cannot be about.
-
Route.
{format}must bepdf,jpg,pngorwebp. Otherwise404 {"message":"Not Found"}, with or without a key. -
A key at all. Otherwise
401 {"message":"Unauthorized"}. -
The key. It must resolve, be in date, and the plan's
monthly allowance must have room. Otherwise
403withToken invalid,Token expiredorLimit exceeded. -
The request.
{scope}must bepublicorprivateand the template must exist. Otherwise400withInvalid template typeorInvalid template name. Sizes are checked here too — see below.
Two consequences. A typo in {format} is a 404 before your
key is even looked at. And a typo in {scope} or the template
id with a working key is a 400, not a 403 — but
with a broken key you will see the 403 from gate 3 first, and the 400
only once the key is fixed.
The one 403 in gate 4 is about your plan, not your key: on the free plan
a render can be at most 1200 px on either side, and anything larger
answers A size larger than 1200x1200 requires a paid
subscription. Past the hard limits — 5000 px for PDF, 3200 px for
images — it is a 400 on any plan.
Every render endpoint also accepts GET, with
variables, size and format as
query parameters, so a method change is not something to debug either.
Do not call this from the browser
A preflight from any origin gets a permissive answer. Read off the live API on 2026-11-01:
access-control-allow-origin: *
access-control-allow-methods: GET,POST,OPTIONS
access-control-allow-headers: authorization,content-type
The actual response then carries an origin decided by the
allowed domains on your account. With none set, it
echoes whatever origin called, so a fetch() from any site
with your key in it works. With domains set, an origin outside the list
gets https://api.ireceipt.pro instead, and the browser
refuses to hand the response to the page.
That check is the browser's, not the server's: the render still runs and
still counts against your allowance, and anything outside a browser —
curl, a script — ignores it entirely. Allowed domains stop
someone else's site from embedding calls with your key; they do not make
a key in frontend code private. An API key in frontend JavaScript is in
the bundle and in the network tab, and anyone who wants it has it in
seconds.
If you need a document generated in response to something a user does in the browser, put your own endpoint in between and let it hold the key.
Where to keep the key
Nothing exotic. Environment variable on the server, injected at deploy, not committed:
import { IReceiptPRO } from '@ireceipt.pro/js'
const client = new IReceiptPRO(process.env.IRECEIPT_API_KEY)
The SDK takes the key at construction and sets the header itself, so build the client once at startup rather than per request.
What this page does not cover
-
Key rotation. A key has an expiry date, and after it
the answer is
403 Token expired. What happens to requests already in flight when you delete a key is not covered here. - Scopes. Keys carry none: any key on an account can render any of that account's private templates and every public one.
-
Rate limiting. There is no per-second limit and no
RateLimit-*orRetry-Afterheader. The limit that exists is your plan's monthly request allowance, and only successful renders count against it.