---
name: hopp
description: Create and manage dynamic QR-code redirects with Hopp (hopp.qr-redirects.be) - make a QR code, re-point where an existing code goes, list codes, or check scan stats. Also order printed QR stickers (a sheet of identical codes, or a series of unique codes) paid from a prepaid credit balance. Use whenever the user wants a QR code they can change later, wants to update/track a Hopp code, or wants to order printed stickers via the API.
---

# Hopp - dynamic QR redirects

Hopp gives you a QR code whose **destination you can change anytime**. The printed code never
changes; you re-point the link behind it. This skill drives the Hopp REST API.

## Setup (once)

1. Sign in at https://hopp.qr-redirects.be and create a key in **Settings → API keys**.
2. Export it so commands can use it:
   ```bash
   export HOPP_API_KEY=hopp_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
   ```

Base URL `https://hopp.qr-redirects.be` · auth header `Authorization: Bearer $HOPP_API_KEY`.
If `HOPP_API_KEY` isn't set, tell the user to create one in Settings and stop.

## Create a code

```bash
curl -s -X POST https://hopp.qr-redirects.be/v1/codes \
  -H "Authorization: Bearer $HOPP_API_KEY" -H 'Content-Type: application/json' \
  -d '{"url":"https://example.com/landing","name":"Launch poster"}'
```
Returns `{ "slug", "short_url", "destination", "qr_png", "qr_svg" }`. Give the user the **short_url**
(the link to print/share) and the **qr_png**/**qr_svg** (the QR image). Don't construct URLs yourself.

## List codes

```bash
curl -s https://hopp.qr-redirects.be/v1/codes -H "Authorization: Bearer $HOPP_API_KEY"
```
→ `{ "data": [ { "slug", "status", "destination_url", "short_url" }, … ] }`

## Re-point a code (no reprint)

```bash
curl -s -X PATCH https://hopp.qr-redirects.be/v1/codes/SLUG \
  -H "Authorization: Bearer $HOPP_API_KEY" -H 'Content-Type: application/json' \
  -d '{"url":"https://example.com/new-destination"}'
```
Free plan: one change per code per month (otherwise `403 {"error":"change_limit"}`); Pro is unlimited.

## Scan stats

```bash
curl -s https://hopp.qr-redirects.be/v1/codes/SLUG/stats -H "Authorization: Bearer $HOPP_API_KEY"
```
→ `{ "slug", "scans" }`

## Scan webhooks (Pro) - trigger an action on every scan

Point a code at the user's HTTPS endpoint; Hopp POSTs a signed JSON payload each time it's scanned.
Use this when the user wants a scan to *do something* (notify, log, kick off automation).

```bash
# set / update - returns the signing secret ONCE; store it to verify signatures
curl -s -X PATCH https://hopp.qr-redirects.be/v1/codes/SLUG \
  -H "Authorization: Bearer $HOPP_API_KEY" -H 'Content-Type: application/json' \
  -d '{"webhook_url":"https://your-app.com/hooks/hopp"}'
# clear it
curl -s -X PATCH https://hopp.qr-redirects.be/v1/codes/SLUG \
  -H "Authorization: Bearer $HOPP_API_KEY" -H 'Content-Type: application/json' -d '{"webhook_url":null}'
```
→ `{ "slug","destination","webhook_url","webhook_secret":"hopp_whk_…" }` (you can also pass
`webhook_url` to `POST /v1/codes` at create time). Pro only - `403 pro_required` otherwise.

Each delivery: `POST` with header `X-Hopp-Signature: sha256=HMAC_SHA256(secret, rawBody)` and body
`{ "event":"scan","slug","destination","scanned_at","referrer","user_agent" }`.
**Verify the signature** before acting: recompute the HMAC over the raw body and compare.

## Order printed stickers from a prepaid balance

Hopp can print and ship QR stickers, paid from a **prepaid credit balance** - top up once, then
place orders with no per-order checkout. Two shapes: a **sheet** of identical codes, or a **series**
of unique codes printed across sheets. Use this when the user wants physical stickers via the API.

```bash
# 1) check the balance (and recent ledger)
curl -s https://hopp.qr-redirects.be/v1/credits -H "Authorization: Bearer $HOPP_API_KEY"
#  → { "balance_cents", "currency", "recent":[…] }

# 2) top up - returns a Stripe Checkout link to open (amount_cents 500-200000)
curl -s -X POST https://hopp.qr-redirects.be/v1/credits/topup \
  -H "Authorization: Bearer $HOPP_API_KEY" -H 'Content-Type: application/json' \
  -d '{"amount_cents":5000}'
#  → { "checkout_url", "amount_cents" }  - give the user checkout_url; balance updates after payment

# 3) quote first (no charge). type=sheet|series; size in mm. Returns total + whether credits cover it
curl -s -X POST https://hopp.qr-redirects.be/v1/orders/quote \
  -H "Authorization: Bearer $HOPP_API_KEY" -H 'Content-Type: application/json' \
  -d '{"type":"sheet","content":{"kind":"url","url":"https://ex.com"},"size":{"width_mm":50,"height_mm":50},
       "copies":2,"recipient":{"name":"Jane","line1":"1 Main St","city":"Gent","postal":"9000","country":"BE"}}'
#  → { "total_cents","vat_cents","sheets","per_sheet","credit_balance_cents","sufficient" }

# 4) place it - ALWAYS send an Idempotency-Key so a retry never double-charges
curl -s -X POST https://hopp.qr-redirects.be/v1/orders \
  -H "Authorization: Bearer $HOPP_API_KEY" -H "Idempotency-Key: $(uuidgen)" -H 'Content-Type: application/json' \
  -d '{"type":"series","items":["A-1","A-2","A-3"],"prefix":"https://ex.com/p","size":{"width_mm":30,"height_mm":30},
       "recipient":{"name":"Jane","line1":"1 Main St","city":"Gent","postal":"9000","country":"BE"}}'
#  → 201 { "order_id","status","charged_cents","credit_balance_cents","invoice_url" }
#  → 402 { "error":"insufficient_credits","required_cents","topup_url" }  (tell the user to top up)
```

Quote before placing, and confirm the total with the user. Track an order with
`GET /v1/orders/:id` (status, tracking, `invoice_url`). A hard fulfilment failure auto-refunds the
credits and returns `502 fulfilment_failed`.

**Auto-recharge (optional).** So orders never stall on an empty balance, the account can save a card
once on the billing page (interactive Stripe setup) and then enable off-session top-ups:
```bash
curl -s -X POST https://hopp.qr-redirects.be/v1/credits/autorecharge \
  -H "Authorization: Bearer $HOPP_API_KEY" -H 'Content-Type: application/json' \
  -d '{"enabled":true,"threshold_cents":2000,"amount_cents":5000}'
#  → { "enabled","threshold_cents","amount_cents","has_card" }   (400 no_card if no card saved yet)
```
When the balance would dip below `threshold_cents`, Hopp charges the saved card for `amount_cents`.

## Good to know

- Slugs are **host-scoped**: a code resolves only on its own domain.
- Free plan allows 1,000 redirects/day; beyond that codes still work behind a short countdown page.
- Top-ups are a VAT-free advance payment; each print order gets its own VAT invoice at `invoice_url`.
- Errors are JSON `{ "error", "message"? }`: `401 invalid_api_key`, `400 invalid_url` /
  `nothing_to_update` / `invalid_webhook_url` / `invalid_recipient` / `idempotency_key_required`,
  `402 insufficient_credits`, `403 change_limit` / `pro_required`, `404 not_found`,
  `502 fulfilment_failed` (credits refunded).
- Equivalent tooling: the `hopp` CLI (`npx @hopp/cli create|point|list|stats|credits|topup|quote|order|autorecharge`,
  or `npm i -g @hopp/cli`) and an MCP server (`node cli/mcp.mjs`) exposing `create_code` / `point_code` /
  `list_codes` / `get_stats` / `get_credits` / `quote_order` / `place_order`.
- Full reference: https://hopp.qr-redirects.be/developers
