qrsalt

QRSalt nodes for n8n: generate QR codes and short links, render and read QR code images, re-point a printed QR code, read its scan analytics, and trigger on every scan.

Documentation

n8n-nodes-qrsalt

Make QR codes and short links in n8n, render and read QR code
images, re-point a code you have already printed, read its scans, and start a
workflow the moment somebody scans one.

Two nodes and a credential ship in the package:

  • QRSalt — the action node: 27 operations across QR codes, short links,
    images, analytics, folders, tags, UTM presets, forms and menus.
  • QRSalt Trigger — fires on a scan, a new code, a change, or a form answer.
  • QRSalt API — one API key, shared by both.

Three levels of access. Two operations need no account at all: drop the node
in and run it. A free API key adds the full renderer. Everything that stores,
lists, changes or measures a code needs a plan with API access
(prices).

You have You can run
No account QR Image → Render (Free), Read (Free)
A free API key …and QR Image → Render, with every format, size and design option
A plan with API access (Pro and above) …and everything else below
A plan with webhooks (Business) …and the QRSalt Trigger

Everything the nodes do

Resource Operations
QR Code Create · Get · Get Many · Update · Delete · Change Many · Get Image · Get Scans
QR Image Render · Render (Free) · Read (Free)
Short Link Create · Get Many
Analytics Get
QR Form Get Many · Get Responses
QR Menu Get Many
Folder Create · Get · Get Many · Update · Delete
Tag Get · Get Many · Update · Delete
UTM Preset Get Many
QRSalt Trigger Code Scanned · Code Created · Code Updated · Code Disabled · Form Answered

So: make a code, change where it points after it is printed, delete it, draw it
as a file, read the text back out of a picture of one, list and search what you
have, and count the scans — with a trigger for the moment a scan happens.

The action node is marked usable as a tool, so an AI Agent node can drive it.

Install

From the editor, on Cloud or self-hosted. Settings → Community nodes →
Install, then n8n-nodes-qrsalt. An instance owner or admin has to do this.

Self-hosted, by hand. In your n8n data folder:

npm install n8n-nodes-qrsalt

Then restart n8n. Node.js 22 or newer. Both nodes then appear in the node panel
under QR, QR Code or QRSalt.

Credential

Make a key in the QRSalt dashboard under Settings → API keys. It starts
qr_live_ and is shown once.

In n8n, add a QRSalt API credential:

Field What to put
API Key The qr_live_… key
Base URL https://app.qrsalt.com unless you are on a staging or self-hosted instance

Saving tests the key. A wrong or revoked key is rejected there and then. A valid
key on a plan without API access is told the truth: it works, but only for
QR Image → Render.

Any plan can make a key. What it may do depends on the plan, and is checked on
every request — a key on a plan that later lapses stops working.

Examples

Five workflows that show the range. The JSON for the first three is in
examples/ and imports straight into the editor.

Print a batch of codes from a spreadsheet

Google Sheets → Read rows → QRSalt → QR Code → Create (Destination
{{$json.url}}, Name {{$json.title}}) → QRSalt → QR Code → Get Image
(Code ID {{$json.data.id}}, Format PDF, 40 mm) → Gmail → Send, the file
attached.

One dynamic code per row, each as a print-ready PDF. Because they are dynamic,
any of them can be re-pointed later without reprinting.

Re-point a printed code when the campaign ends

Schedule Trigger, daily → QRSalt → QR Code → Get Many (Filters → Tag
spring-campaign) → QRSalt → QR Code → Update (Update Fields → Destination
https://example.com/whats-on-now, Note spring ended).

Every printed poster follows from the next scan. Nothing is reprinted. Use
Change Many instead when you only need to pause or re-file them — it takes
up to 500 IDs in one call.

Post to Slack when a code is scanned

QRSalt Trigger, Events Code Scanned → Slack → Send message:
{{$json.data.slug}} scanned in {{$json.data.city}}.

Activating the workflow registers the webhook with QRSalt; deactivating it
removes it again. Every delivery's signature is verified before the workflow
runs.

Read a code out of an uploaded image and look it up

Webhook (or Gmail / Drive / Form trigger) → QRSalt → QR Image → Read
(Free)
(Input Binary Field data, Look For QR Code) → QRSalt → QR Code →
Get Many
(Search {{$json.data}}) → IF: known code, or one to flag.

The read needs no account, so this half runs before anybody signs up. Set Look
For to Every Symbology Below to accept barcodes on packaging too.

Weekly scan report to a sheet

Schedule Trigger, Mondays → QRSalt → Analytics → Get (From
{{$now.minus(7, 'days').toFormat('yyyy-MM-dd')}}, Breakdown Country) →
Google Sheets → Append.

Totals and a daily series for the whole workspace. For one code instead, use
QR Code → Get Scans with the code's ID.

Try every operation in one run

examples/zzz-full-test.json exercises the whole
node against a real workspace and tidies up after itself. Use it to prove a new
key, a new plan or a new release end to end.

  1. Make a credential of type QRSalt API before you import. The file
    carries no credential of its own, so n8n binds yours to every QRSalt node as
    the workflow comes in and says so in a toast.
  2. Workflows → Import from file.
  3. Fill in the one node n8n leaves alone: open Check Credential and pick
    your credential there. It is an HTTP Request node, and n8n never binds those
    for you. Any other node still showing an empty Credential field has to be
    filled in by hand before the run means anything.
  4. Press Execute workflow once.
  5. Open the last node, Summary, and copy its single report field.

The first line is the verdict. Every line after it is one step, and it can only
say one of three things:

  • ok — with the data it produced. A line says ok only when the field that
    step exists to produce is actually in the answer.
  • FAIL — with the API's own sentence, or the name of what was missing.
  • SKIPPED — the step's input never existed, so it was not tried. Never a pass.

The run starts with one cheap authenticated call, so a key that is refused, or
on a plan without API access, is named on the first line and everything under it
reads SKIPPED instead of pretending to have run. A step the API turns down
outright is different: that node goes red and the run stops there, so there is no
report at all and the node's own error is the answer.

RESULT: PASSED — every step returned what it should
QRSalt n8n node test — 2026-09-18T04:12:07.881Z — run 20260918041201
------------------------------------------------------------------------
credential            ok       workspace=Acme plan=PRO
render                ok       image/png 4210 B
readFree              ok       read back "https://example.com/?zzz-test-…" format=qr
codeCreate            ok       id=a4705538-… slug=pKvEYtm link=https://qrsalt.com/pKvEYtm
codeGetScans          ok       1 scans, 1 unique
codeChangeMany        ok       action=status done=2 unchanged=0 skipped=0
…
cleanup               ok       4 of 4 objects removed
------------------------------------------------------------------------
FAILURES: none

A run that cannot work says so instead:

RESULT: FAILED — 1 step did not work
credential            FAIL     credential not working — HTTP 401 API key is invalid or revoked
renderFree            ok       image/png 4210 B
codeCreate            SKIPPED  credential not working, so this was not tried

Everything it makes is named zzz-test-<timestamp>-…. It creates two QR codes, a
short link and a folder, uses them, and deletes every one of them again. The key
needs the delete scope, which is never ticked on a new key; without it the
run stops at the first delete and leaves those objects behind for you to remove.

A trigger cannot fire from a manual execution, so it ships separately as
examples/zzz-trigger-test.json: one trigger
node subscribed to all five events, a Delivery Received node that judges
whatever arrives, and one branch per event below it, so the execution says at a
glance which event came in. Import it, check the credential on the trigger node,
and switch the workflow on — activating is what registers the endpoint with
QRSalt. QRSalt has to reach your n8n from the internet, so on a laptop start it
with n8n start --tunnel, and the workspace needs a plan that includes webhooks.
Then run zzz-full-test: it requests the short link it created, which is a real
scan, and scan.recorded arrives here. examples/README.md
says how to fire the other four. Delivery Received carries a one-line
result and a fuller report; either says PASSED only when a delivery really
arrived carrying the fields its event is supposed to have, and names what was
missing when it did not.

Reference

Every operation is one call to the QRSalt REST
API
. Answers arrive in the API's envelope: the
result under data, paging and notes under meta.

QR Code

Operation Key fields Returns
Create Type, then that type's own content fields (Destination for a website code), Name, plus Kind, Slug, Domain, Folder, Tags, Status The new code under data, with id, shortUrl, image and publicImage
Get Code ID The code
Get Many Limit, Offset, Search, Filters (status, tag, folder, domain, output) A list, with meta.nextOffset
Update Code ID, then Content Type and that type's fields to change what the code holds, and any of Destination, Name, Slug, Domain, Folder, Tags, Status, Note The updated code
Delete Code ID Confirmation
Change Many Code IDs (up to 500), Action: status, folder, domain, tags, UTM preset or delete Each ID under results, done or skipped with a reason
Get Image Code ID, Format, Size or Printed Width The file, in the binary field you name
Get Scans Code ID, From, To, Breakdown Totals and a daily series

Every code comes back with two image addresses. image is the API endpoint,
which needs your API key in an Authorization header — right for another
request, useless anywhere a person pastes a URL. publicImage is the same
picture at a link that opens for anyone: no key, no header, so it works in an
email, a Google Sheets cell, a Slack message or an <img src>. It serves the
picture and nothing else — no name, no destination, no scan count — as a PNG, or
as SVG if you swap the extension. Treat it as public: the address is signed, so
it cannot be guessed from the slug, but anyone you send it to can open it and
pass it on. It never changes, and it stops working the moment the code is
deleted.

Kind decides everything downstream. A dynamic code (the default) is the
one worth printing: the scan goes through QRSalt, so Update changes where every
printed copy lands. A static code carries its destination inside the pattern
and can never be changed.

Type covers URL, Text, vCard, Wi-Fi, Email, SMS, Phone, Location, Calendar
Event, Payment Link, Review Request, App Store Link and GS1 Digital Link.
Choosing one reveals the fields that type carries — a network name and
encryption for Wi-Fi, a first name and a company for a contact card, a start
time and a zone for an event — and the node sends them as the payload object
the API validates. A website code keeps the Destination box it has always
had, so a workflow built against an earlier version needs no change.

PDF file codes are not offered. Their content names a file on QRSalt's
servers and the key for one is minted by the upload endpoint, so there is
nothing this node could collect: upload the PDF in the dashboard, or call
POST /api/v1/codes yourself with the HTTP Request node. Gallery and
audio codes are not built yet and the API refuses them.

Update changes content the same way: pick the Content Type the code
already is and fill the fields you want it to hold. What you fill in replaces
the code's content, so anything you leave empty is cleared; leaving Content Type
on Unchanged touches nothing, which is what an Update that only renames or
re-files a code has always done. A content type that disagrees with the code is
refused by name rather than quietly changing nothing. Destination on Update
is the website code's content under another name — on any other type it is a key
that payload does not have, so the call answers 200 and nothing changes.

Get Image returns the saved code as SVG, PNG, JPG, WebP or PDF — with its
dashboard design, colours and logo — straight into a binary field, so the next
node can attach or upload it with no download step between.

Delete cannot be undone, and there is nothing to type before it runs. The
guard is the key: deleting needs one made with the Delete permission, which
is never ticked by default and which the Write permission does not cover. A
workflow is written on purpose by a person choosing this operation, and a box to
tick is one somebody ticks once and then forgets.

Deleting a dynamic code stops every printed copy at the next scan, and its
short link is never handed to anyone else. Deleting a static code removes
only the saved record: the printed copies carry their destination inside the
pattern and keep working.

Change Many → Delete Them does the same to up to 500 codes in one call, and
wants the same Delete permission. An ID that is not one of yours is reported
as skipped, never deleted, and every short link it retires stays retired.

Get Scans takes at most 366 days at a time, and your plan's retention can
shorten the range — when it does, meta.clamped is true and meta.note says so.

QR Image

Operation Needs Content Formats
Render Any API key Up to 2000 bytes SVG, PNG, JPG, WebP, PDF
Render (Free) Nothing Up to 512 bytes SVG, PNG
Read (Free) Nothing An image in a binary field —

Render draws a static code on the spot and hands it back as a file. Nothing
is stored and nothing can be changed afterwards, so use it for a ticket, a label
or a Wi-Fi card — and QR Code → Create for anything going on a poster.

Field Range
Size 64–2000 px (Render Free: 64–512)
Printed Width 5–1000 mm, PDF only
Error Correction L, M, Q or H
Margin 0–20 modules
Foreground / Background / Eye Colour Hex
Module Shape Square, rounded, dots, classy
Eye Shape / Eye Frame / Eye Ball Square, rounded, circle, drop, leaf, leaf flipped
Eye Rotation Upright or a half turn

Render (Free) offers colour, margin, error correction and size only; shapes and
the larger formats are on Render. Neither draws a logo — a logo belongs to a
saved code, so design it in the dashboard and fetch it with QR Code → Get Image.

Read (Free) takes a PNG, JPEG or WebP out of a binary field — up to 12 MB,
which is a phone photo at full resolution — and returns the text inside the
code. Nothing is installed in your n8n, and nothing is kept at the other end
either: the image is read in memory and then discarded, never written to disk
and never logged, and neither is the text found in it.

Look For Reads
QR Code QR codes. The default.
Code 128 Code 128, GS1-128 included
Code 39 Code 39
EAN-13 EAN-13, and UPC-A and ISBN, which are carried as one
ITF Interleaved 2 of 5, which is what an ITF-14 is
Data Matrix Data Matrix
PDF417 PDF417
Aztec Aztec
Every Symbology Below All eight in one pass

That is the whole list: EAN-8, UPC-E, Codabar, MaxiCode and DataBar are not
read, and neither are PDFs or video. One code per image.

The output item:

{
  "success": true,
  "data": "https://example.com/spring-menu",
  "format": "QRCode",
  "version": 4,
  "errorCorrection": "M",
  "mask": 2
}

data is the text and format is what was found, so a run over a mixed folder
can branch on it. The last three are what the symbol says about itself, and are
null for symbologies that carry none — a Code 128 has all three null.

Rate limits

The keyless endpoints are counted by the work rather than the request: an SVG
costs 1 unit, a PNG up to 256 px 2, a larger PNG 4. One caller gets 480 units an
hour and one address 4,800. Over that is a 429 carrying resetAt and a
Retry-After.

Because n8n Cloud workflows leave through addresses shared with every other n8n
user, the free operations send an X-QRSalt-Client header — a hash of your
instance's id, carrying nothing about you — so your allowance is counted apart
from theirs. It is for sharing the allowance fairly and nothing else.

A key removes the problem: its limit is yours and nobody else can spend it. A
key on Pro or above can make 600 requests a minute; a render-only key on a
cheaper plan, 60. Change Many is limited to 60 requests an hour per workspace on
top of that. Make a free key at
app.qrsalt.com/dashboard/api and use
Render instead of Render (Free).

Short Link

Operation Key fields Returns
Create URL, plus Slug, Domain, Name, Folder, Tags The link, with id and shortUrl
Get Many Limit, Offset, Search A list, with meta.nextOffset

A short link is the same record as a dynamic QR code — same re-pointing, same
scans — so manage it afterwards with the QR Code operations, using the id from
the create answer. Note the one difference in spelling: links take URL,
codes take Destination.

Get Many lists the short links only: what was made as a link, which comes
back as output: "link". QR Code → Get Many is the one that lists everything in
the workspace, and its Has A Short Link filter is the wider question — every
dynamic code, because each of those has a short link too.

Analytics

Get returns totals and a daily series across every code in the workspace,
with one optional breakdown: Country, Region Code, City, Device, OS, Browser,
Referrer, Hour or Weekday. A region comes back as its ISO 3166-2 subdivision
code (CA, 03), which is what the network reports. Country is on every plan; the rest need advanced
analytics, and asking for one you do not have is refused by name rather than
quietly returning less. From and To are YYYY-MM-DD, at most 366 days apart,
defaulting to the last 30 days.

QR Form, QR Menu, Folder, Tag, UTM Preset

Operation Returns
QR Form → Get Many Each form with its view and response counts
QR Form → Get Responses Answers, newest first, with the questions under meta.questions; Limit 1–100 and Offset
QR Menu → Get Many Each menu with its public URL, whether it is published, and its views
Folder → Create The new folder
Folder → Get One folder and how many codes are in it
Folder → Get Many The folders, with the IDs the code operations ask for
Folder → Update The folder under its new name
Folder → Delete codesUnfiled: how many codes moved out. They are not deleted
Tag → Get One tag and how many codes carry it
Tag → Get Many The tags
Tag → Update The tag under its new name, on every code carrying it
Tag → Delete codesUntagged: how many codes lost the word. They are not deleted
UTM Preset → Get Many The presets, with the ID Change Many → Apply UTM Preset needs

Forms and menus are built in the dashboard; the node reads them. Tags have no
create of their own — naming one on a code or in a bulk action makes it.

Deleting a folder or a tag deletes neither the codes nor anything printed. A
folder's codes move to "no folder"; a tag's codes lose the word. Both need a key
with the delete permission, which is never ticked for a new key. For
form answers as they arrive, use the trigger's Form Answered event.

QRSalt Trigger

Needs a plan that includes webhooks (Business). Activating the workflow registers its
webhook URL with QRSalt and stores the signing secret; deactivating it removes
the endpoint again, so nothing is left behind failing.

Event Fires when
Code Scanned Somebody scanned a code or opened a short link
Code Created A code or short link was made
Code Updated A code was re-pointed, or its link, domain or campaign tags changed — renaming one sends nothing
Code Disabled A code was paused, deleted or disabled
Form Answered A QR Form was answered — the answers are in the payload

A scan arrives with its country, regionCode, city, device, OS, browser and
referrer host under data. regionCode is the ISO 3166-2 subdivision code the
network reports — 03, CA — not a region name; the country is beside it.

Every delivery carries x-qr-signature: t=<unix seconds>,v1=<hex hmac-sha256>
over ${timestamp}.${body}. The node verifies it against the raw request bytes
before the workflow runs, and refuses anything older than five minutes with a
401 even when the signature is genuine.

Two things to build around. Delivery is at-least-once: the same event can arrive
twice, so deduplicate on the x-qr-event-id header, and one can be missed, so
reconcile with Get Scans if you need exact numbers. And QRSalt has to reach your
n8n from the internet — a localhost or private-network instance is refused when
the endpoint is registered.

When something goes wrong

A refused call fails the step. It is never handed on as an item carrying the
error text, which would read as a step that worked. If the step is set to
continue on error, the sentence arrives in json.error and the item is marked
failed, so the workflow can branch on it without mistaking it for an answer.

Message What it means
This node has no QRSalt API credential attached… No credential is picked on the node. An imported workflow has none until each node is opened once — n8n attaches yours then, not on import. Only QR Image → Render (Free) and Read (Free) run without one
The QRSalt API credential on this node has no API key in it. The credential is picked but its API Key field is empty
This node's QRSalt API credential could not be read… A credential is attached and n8n would not hand it over. The message carries what n8n said
This node's API key never reached QRSalt. The credential holds a key and the request still went out with no Authorization header. Open the node once and run again
QRSalt refused this API key. A credential is picked, but the key is wrong or revoked — or the credential's Base URL is not the instance the key was made on
That API key is not valid. Wrong, revoked, or its workspace is gone
… over the API comes with Pro and above. The key is fine; the plan does not include API access
This API key does not have the "write" scope. The plan is fine; the key was made narrower. Make a wider one under Settings → API keys
Webhooks are not included in … The trigger needs a plan with webhooks
A static code … cannot be repointed. Only dynamic codes can be updated. Create a new one
Too many requests. Slow down… Rate limited; the answer carries retry-after
This API key does not have the "delete" scope. Deleting, one code or many, needs a key made with the Delete permission
No QR code was found in that image. Read (Free) found no code of the kind you asked for; it says to add formats=all to look for barcodes too
That file is not a PNG, JPEG or WebP image we can read. Read (Free) judges the file by its bytes, not its name
"color" must be a hex colour, for example 1F2937. A render parameter is wrong. The API names the one it is
"size" must be a number between 64 and 512 without an API key. Render (Free) caps the size; the hint says larger exports are on the keyed Render
HTTP 413 from Read (Free) The image is over 12 MB
The range was shortened to what your plan retains. Not an error: meta.clamped on an analytics answer

Every refusal keeps the API's own sentence and adds what to do about it. QRSalt
answers in two shapes — the managed endpoints nest the refusal, the render and
read endpoints put the sentence straight in error — and both are read, so a
bad parameter on QR Image says what is wrong with it rather than HTTP 400. A
hint arrives with it when the API sends one.

Development

npm install
npm run build
npm run lint
npm test
npm run dev       # runs n8n locally with these nodes loaded (needs Docker)

Issues and pull requests:
QRSalt/n8n-nodes-qrsalt.

Licence

MIT.

Discussion