logo
APIAutomations

Send link and QR code events to your app with webhooks

Create a CodeQR webhook, pick the events, verify the CodeQR-Signature header, handle retries and failures, and read the delivery log — with a real payload.

Avatar for undefined
CodeQR Team
Content Team

A webhook is an HTTP POST that CodeQR sends to a URL you own the moment something happens — a link is created, a QR code changes, a lead arrives, someone clicks. By the end of this guide you have a webhook receiving signed events, you can prove each event is genuine, and you know what CodeQR does when your endpoint fails.

Availability

  • Plan: Pro and above for created/updated/deleted events on links, QR codes and pages, and for Lead created / Sale created. Link clicked, QR Code scanned and Page visited — and attaching a webhook to specific links, codes or pages — need Business or above.
  • Where: SettingsDeveloper SettingsWebhooks. Only the workspace owner can create, edit or delete webhooks; members can read the list and the logs. Reference: Webhooks on docs.codeqr.io.

Before you start

  • A public HTTPS URL that answers 2xx quickly. For a first test use a request inspector such as https://webhook.site (it gives you a unique URL and shows what arrives) or expose a local server with a tunnel.
  • Decide the events you need. Project-level events fire for every link, code or page in the workspace; link-, QR-code- and page-level events fire only for the ones you attach.
  • One webhook per URL per workspace: creating a second webhook with the same URL updates the first.

Steps

  1. Open SettingsWebhooks and click Create Webhook.
  2. Fill Name (up to 30 characters) and URL. The Signature secret is generated for you (whsec_ + 32 hex characters) — copy it into your server's environment now; it stays visible on the webhook's page later, and it is what you will use to verify deliveries.

Create webhook form: Name, URL, Signature secret and the project-level events

  1. Tick the events: - Project-level eventsThese events are triggered at the project level.: Link created, Link updated, Link deleted, QR Code created, QR Code updated, QR Code deleted, Page created, Page updated, Page deleted, Lead created, Sale created. - Link-level events (High traffic)Link clicked; ticking it reveals Select the links for which events should be sent with every link listed as <date> - <domain/key>. Tick the links you want. - QR Code-level events (High traffic)QR Code scanned, with the same picker for QR codes. - Page-level events (High traffic)Page visited.

Link-level and QR Code-level events with the pickers of links and QR codes

  1. Click Create webhook. The list shows the new webhook as Ativado (the status labels are in Portuguese in every language today).
  2. Open the webhook. (Abrir opções) offers Copiar ID do Webhook, Enviar evento de teste, Desativar webhook and Excluir webhook; the tabs are Logs do Webhook and Atualizar Detalhes.

Webhook options menu

  1. Send a test: Enviar evento de teste → choose an event in Send Test Webhook EventSend webhook test. Your endpoint receives a sample payload for that event.

Send Test Webhook Event dialog

  1. Create a link (in the dashboard or with the API). Within a few seconds a real link.created event lands, and the row appears in Logs do Webhook with the HTTP status your endpoint returned; click a row to see Response and Request.

Webhook event log with 200 rows and the request/response panel

What you receive

Headers:

Content-Type: application/json
User-Agent: Go-http-client/2.0
CodeQR-Signature: b66393f98f01e7cba58a20ea4654ee66c5c72ff3a8e09db4fcd73624560e4b25
CodeQR-Project-Id: cm73y7wm100008j24i1b137wr

Body — the envelope is always id, event, createdAt, data; inside data you get eventName and the object the event is about (link, qrcode, page, customer for leads, sale) and, for clicks and scans, an interaction object with the click details (country, device, browser, referer, click_id). A real link.created delivery, trimmed:

{
  "id": "evt_YT2m0xh9aUWRTV1FkfNTvrvls",
  "event": "link.created",
  "createdAt": "2026-08-17T16:53:22.473Z",
  "data": {
    "eventName": "Link created",
    "link": {
      "id": "cmsxh367q0003wu8tzav2adsp",
      "domain": "go.example.com",
      "key": "docs-summer",
      "url": "https://example.com/summer-sale?utm_source=newsletter&utm_medium=email&utm_campaign=summer",
      "shortLink": "https://go.example.com/docs-summer",
      "externalId": null,
      "tags": [],
      "clicks": 0,
      "createdAt": "2026-08-17T16:53:20.822Z",
      "updatedAt": "2026-08-17T16:53:20.822Z"
    }
  }
}

The link object is the same shape the API returns; qrcode events carry the QR code object (type, static, colors, image…). Full field lists per event: Webhook events.

Verify the signature

CodeQR-Signature is the HMAC-SHA256 of the raw request body, keyed with your whsec_… secret, as lowercase hex. Compute it over the exact bytes you received — before any JSON parsing or re-serialisation — and compare with a constant-time comparison. Both snippets below were checked against real deliveries.

Node.js (Express, raw body):

import crypto from 'node:crypto';
import express from 'express';

const app = express();
app.post('/codeqr', express.raw({ type: 'application/json' }), (req, res) => {
  const expected = crypto
    .createHmac('sha256', process.env.CODEQR_WEBHOOK_SECRET)
    .update(req.body) // Buffer with the raw bytes
    .digest('hex');
  const received = req.get('CodeQR-Signature') || '';
  const ok = received.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
  if (!ok) return res.status(401).send('bad signature');

  const event = JSON.parse(req.body);
  // handle event.event / event.data here, then answer fast
  res.status(200).send('ok');
});

Python:

import hmac, hashlib

def is_valid(raw_body: bytes, header: str, secret: str) -> bool:
    expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, header)

Reject anything that does not verify; a request without the header is not from CodeQR.

Retries, duplicates and failures

  • Deliveries are queued and retried when your endpoint answers 4xx/5xx or times out, so the same event can arrive more than once. Use id (evt_…) as an idempotency key and ignore repeats.
  • Answer 2xx within a few seconds and do the heavy work afterwards; a slow endpoint is treated as failing.
  • After 5, 10 and 15 consecutive failures the workspace owner receives an e-mail (Webhook is failing to deliver); at 20 the webhook is disabled and the owner receives Webhook has been disabled. Fix the endpoint, then re-enable it from Ativar webhook on the webhook page. Successful deliveries reset the counter.
  • Zapier and Make endpoints that answer 410 unsubscribe the webhook — that is how those platforms turn a trigger off.

Do the same via API

With a key that has Webhooks → Write (owner-only):

curl https://api.codeqr.io/webhooks \
  -H "Authorization: Bearer codeqr_••••••••••••••••••••••••"

returns each webhook with id, name, url, secret, triggers, linkIds, qrcodeIds, pageIds, disabledAt. POST /webhooks creates one (name, url, triggers, optional linkIds/qrcodeIds/pageIds, optional secret); PATCH /webhooks/{id} and DELETE /webhooks/{id} edit and remove. Make's Watch Webhook Events module and the Pluga integration create webhooks this way for you.

Verify it works

  1. Create a link. Your endpoint logs one POST; Logs do Webhook shows a 200 row for link.created within seconds.
  2. Your signature check accepts it. Change one byte of the secret and it must fail.
  3. Send the same event id twice through the test button and confirm your handler processes it once.

Troubleshooting

Signature never matches

You hashed the parsed and re-serialised JSON, or a framework already turned the body into an object. Hash the raw bytes (Express express.raw, Next.js await req.text(), Django request.body); make sure you use this webhook's secret, not another webhook's or the API key.

The webhook is Desativado

Twenty consecutive failed deliveries disable it (you got two e-mails before). Fix the endpoint — check Logs do Webhook for the status codes — and re-enable from Ativar webhook.

No event arrives for clicks or scans

Link clicked and QR Code scanned only fire for the links and codes you ticked in the picker, and only on Business and above. Also see the note at the top: at the time of writing these deliveries were being blocked by a server-side validation bug (fix in review); created/updated/deleted events are unaffected.

The test event for "Link clicked" has no click details

The test payload contains the link object only; real click events add interaction. Test with a real click on an attached link.

Bulk-created links did not trigger link.created

Links created through POST /links/bulk did not fire the event in our test; single creates and dashboard creates did. Trigger your automation from the bulk response instead.

The form says "This webhook is managed by an integration"

Webhooks created by Zapier, Make or Pluga can only have their triggers and attached links changed here; the URL belongs to the platform. Change or remove them from the platform side.

Related articles