Qrop logoQrop

Send QR code scans to Slack, Google Sheets or your CRM with webhooks

By Qrop Team ·

Every time someone scans a tracked Qrop code, we record it. Until now you had to open the dashboard, or pull the numbers through the API, to see it. With webhooks, Qrop posts the scan to an address you choose about a minute after it happens, so it can land in a Slack channel, a spreadsheet row or a CRM record without anyone checking a dashboard.

This guide covers what the request contains, how to add an endpoint, whether to use a no-code tool or write a small receiver, how to check that a request really came from Qrop, and what to do when your server was down. Webhooks are for accounts with purchased credits or a plan, the same as tracked codes.

What a scan webhook actually sends

Qrop sends four kinds of event. scan.logged fires when a scan is recorded. scan_limit.reached fires once, when an account passes its monthly scan allowance. code.expiring fires seven days before a tracked code expires, and code.expired fires after it has. You choose which of them each endpoint receives.

A scan event carries the tracked code's id, its current destination URL, the scan time, and the country, region, city, device type, operating system, browser, language and referrer where Qrop could work them out. Anything unknown is null. It carries no IP address and no visitor hash: Qrop doesn't store an IP address, and the hash behind unique-scanner counts stays internal. So a webhook can tell you a phone in Cape Town scanned the menu code, but never who the person was.

One detail surprises people: the request holds an events array, not a single event. Qrop batches. Every minute it gathers whatever is waiting for your endpoint and sends it in one POST, and each event in the array has its own id, type and timestamp. A busy code therefore costs your server one request a minute instead of one per scan. The price is that a scan reaches you up to about a minute after it happens. Write your receiver to loop over events from the start.

Add an endpoint and test it

You can have up to five endpoints, so one can feed Slack while another feeds your CRM. Each has its own secret, events and headers.

  1. Open Settings in your dashboard and find the Webhooks panel.
  2. Enter the address that will receive events. It must start with https. Addresses with a username or password in them, localhost, and addresses that point at private networks are refused.
  3. Tick the events you want. For scan alerts, scan.logged is enough.
  4. Optionally add up to five headers of your own, such as an Authorization header with a token your server expects. Qrop stores the values encrypted and shows only the names afterwards.
  5. Press Add endpoint and copy the signing secret that appears. It starts with whsec_ and is shown once. If you lose it, New secret replaces it.
  6. Press Send test. Qrop sends a signed ping straight away, without the queue, and shows the status your server answered.

Choose how to receive it: a no-code tool or a small receiver

You have two practical routes. A no-code tool such as Zapier or Make can give you an https address to paste in, then pass the data on to Slack, Google Sheets or a CRM with a few clicks. Zapier calls its trigger a catch hook, and Make has a custom webhook module. This is the quickest route for a team without a developer. Check two things before you rely on it. Some tools don't show you the request headers, which means you can't check the signature. And some treat the events array as a single item, so test with a real scan and look at how the events arrive.

The second route is a small receiver of your own: a function on your server or a serverless platform that accepts the POST, checks the signature, and forwards what it needs. It takes more effort once and gives you full control. It is also the only way to reach Slack, because a Slack incoming webhook expects a message in Slack's own shape, a JSON object with a text field, and Qrop sends an events array. Your receiver turns each scan into a line of text and posts that to Slack.

Whichever you choose, treat the address as a secret. Anyone who knows a no-code tool's hook address can post to it, so don't share it. If your tool can check a header, add a custom header with a long random value on the endpoint and have the tool reject requests that lack it.

Check the signature before you trust a request

If you run your own receiver, check every request before acting on it. Qrop signs the body so you can tell it came from us and wasn't changed on the way.

  1. Read the Qrop-Signature header. It looks like t=1760000000,v1=9f2c... where t is the Unix time in seconds and v1 is the signature in hex.
  2. Take the raw request body exactly as it arrived, before any JSON parsing. Parsing and re-serialising changes the bytes, and the check then fails.
  3. Join t, a dot and the raw body, and compute an HMAC-SHA256 of that string with your whsec_ secret as the key. Write the result in hex.
  4. Compare it with v1 using a constant-time comparison, such as timingSafeEqual in Node.
  5. Reject the request if t is more than about five minutes old. That stops someone replaying a captured request later.

Expect duplicates, and answer quickly

Delivery is at least once. If your server processed a batch but took longer than five seconds to answer, Qrop treats the delivery as failed and sends the batch again. Every event has a unique id starting with evt_, so keep the ids you have handled and skip repeats. A short-lived store is enough, because duplicates arrive within minutes.

Return a 2xx status as soon as you have checked the signature and saved the events, and do the slow work, such as the Slack post or the CRM call, afterwards. Only a 2xx counts as delivered. A redirect counts as a failure, because Qrop never follows one, so if your address redirects from http to https or from a missing trailing slash, give Qrop the final address.

A failed batch is retried after one minute, then five, then every fifteen. After about an hour and six tries Qrop stops retrying that batch. If an endpoint fails continuously for three days, Qrop switches it off and emails you, and you turn it back on in Settings once it works.

What to send where

Slack suits events that deserve a person's attention: scan_limit.reached, code.expiring, and the first scans of a new campaign. Posting every scan from a busy code into a channel is noise, so have your receiver filter by tracked code id or country before it posts.

A spreadsheet suits scan.logged. Append one row per event with the scan time, code id, country, city, device and referrer, and you have a live log you can pivot by week. Because of the duplicates above, store the event id in a column and skip any row whose id is already there.

A CRM suits code.expiring and code.expired. They can create a task for whoever owns the campaign to renew the code, since an expired code stops redirecting. scan_limit.reached works the same way: it is a prompt to talk to the customer about a bigger allowance.

When your server was down

Retries cover a short outage. For a longer one, press Resend missed scans on the endpoint in Settings and choose the last 24 hours or the last 7 days. Qrop rebuilds the scan.logged events from its scan records and queues them for that endpoint, marked replayed: true. They arrive within a couple of minutes like any other delivery. The endpoint has to be on and subscribed to scan.logged, and only scans are replayed, not the limit and expiry events.

A press queues at most 1,000 scans, oldest first, and an endpoint can be replayed once every ten minutes. If a window holds more than that, choose the shorter period, and use the events endpoint of the API (GET /api/v1/tracked/{id}/events, which pages through a code's scans) for the rest.

A replayed event takes its id from the scan itself, so pressing the button twice doesn't produce new ids. A scan you already received live has a different id, though. If exact counts matter, de-duplicate on the code id and scan time as well. Replayed events can also arrive in any order, so sort by the scannedAt time rather than by arrival.

Frequently asked questions

Who can use Qrop webhooks?

Accounts with purchased credits or a plan, the same rule as tracked codes. A free account sees a prompt to buy a credit pack. Each account can have up to five endpoints.

Can I point Qrop straight at a Slack incoming webhook URL?

No. A Slack incoming webhook expects a JSON object with a text field, and Qrop sends a signed request holding an events array. Put a small receiver, or a no-code tool such as Zapier or Make, between the two, and have it post a line of text to Slack for each event you care about.

Why did I receive the same event twice?

Delivery is at least once. If your server is slow to answer or the answer is lost, Qrop sends the batch again. Every event has a unique id starting with evt_, so keep the ids you have handled and ignore repeats.

How soon does a scan reach my endpoint?

Usually within about a minute. Qrop sends the events waiting for an endpoint once a minute, in a single request, rather than one request per scan.

Does a scan event include the visitor's IP address?

No. A scan event has the tracked code, its destination, the scan time, and country, region, city, device type, operating system, browser, language and referrer where known. There is no IP address and no visitor identifier.

What happens to events while my server is down?

Qrop retries each failed batch for about an hour. If an endpoint keeps failing for three days it is switched off and you are emailed. To recover scans you missed, use Resend missed scans on the endpoint for the last 24 hours or 7 days, or page through a code's scans with the events endpoint of the API.

Related guides

Ready to create your QR code?

Generate a free QR code in seconds: custom colours, logos, and PNG, SVG or PDF output.

Open the generator

We use strictly necessary cookies to run Qrop, and (only with your consent) advertising cookies. See our privacy policy for details.