Static QR API documentation

Create a key, call POST /v1/generate from your backend, use the SVG on your site. Step-by-step guide below.

Connect Static QR API

End-to-end path: create a key in WebQR, call the API from your backend, take the SVG from the response, and use it on your site. The API is stateless — WebQR does not store your QR codes.

Step-by-step integration

Follow the steps in order. No SDK — HTTPS + JSON from your server only. Never put the API key in browser JavaScript or a mobile app binary.

  1. Account and plan

    Sign in to WebQR on a plan that includes the Static QR API. Compare included monthly generations and key limits on Pricing.

    Plans and pricing

  2. Create an API key

    In the cabinet open API keys, create a key, and copy the secret once (it is shown only at creation). Optionally restrict by IP and enable required request signing for production.

    API keys

  3. Store the key on your server

    Put the secret in environment variables or a secrets manager. All calls must go from your backend. Never ship the key to the frontend or mobile app.

  4. Call POST /v1/generate

    Send headers X-API-Key and Content-Type: application/json. Body: required string data (URL or text) and optional design object. Examples below — replace the sample key with yours.

    https://api.webqr.io/v1/generate
    curl -X POST https://api.webqr.io/v1/generate \
      -H "Content-Type: application/json" \
      -H "X-API-Key: wq_xxxxxxxxxx_your_secret" \
      -d '{
        "data": "https://webqr.io",
        "design": {
            "size": 512,
            "color": "#000000",
            "backgroundColor": "#FFFFFF",
            "styleType": "a7k2m9",
            "eyeType": "r2s4t6",
            "showColorGradient": false
        }
    }'
    <?php
    $ch = curl_init('https://api.webqr.io/v1/generate');
    curl_setopt_array($ch, [
      CURLOPT_POST => true,
      CURLOPT_RETURNTRANSFER => true,
      CURLOPT_HTTPHEADER => [
        'Content-Type: application/json',
        'X-API-Key: wq_xxxxxxxxxx_your_secret'
      ],
      CURLOPT_POSTFIELDS => '{
        "data": "https://webqr.io",
        "design": {
            "size": 512,
            "color": "#000000",
            "backgroundColor": "#FFFFFF",
            "styleType": "a7k2m9",
            "eyeType": "r2s4t6",
            "showColorGradient": false
        }
    }',
    ]);
    echo curl_exec($ch);
    const res = await fetch('https://api.webqr.io/v1/generate', {
      method: 'POST',
      headers: {
          "Content-Type": "application/json",
          "X-API-Key": "wq_xxxxxxxxxx_your_secret"
      },
      body: JSON.stringify({
        "data": "https://webqr.io",
        "design": {
            "size": 512,
            "color": "#000000",
            "backgroundColor": "#FFFFFF",
            "styleType": "a7k2m9",
            "eyeType": "r2s4t6",
            "showColorGradient": false
        }
    }),
    });
    const data = await res.json();
    console.log(data);
    import requests
    
    r = requests.post(
      "https://api.webqr.io/v1/generate",
      headers={
          "Content-Type": "application/json",
          "X-API-Key": "wq_xxxxxxxxxx_your_secret"
      },
      json={
          "data": "https://webqr.io",
          "design": {
              "size": 512,
              "color": "#000000",
              "backgroundColor": "#FFFFFF",
              "styleType": "a7k2m9",
              "eyeType": "r2s4t6",
              "showColorGradient": false
          }
      },
    )
    print(r.json())
  5. Read the SVG from the response

    On HTTP 200 take data.qr_code (SVG markup) and data.format ("svg"). The usage object shows quota counters for that request. On errors use HTTP status and error.code when present.

    Response

  6. Use the SVG on your site

    Save the file, put it on a CDN, embed inline, attach to email, or send to print. WebQR does not keep the result — you own storage and IDs.

  7. Watch quota and billing

    GET /v1/usage returns counters without spending quota. Check Limits for plan caps; enable API overage billing in the cabinet if you may exceed included generations.

    Usage · API limits · API billing in cabinet

  8. Design, security, errors

    Then tune design fields, lock down the key, and wire error handling. Production checklist:

    • For production, restrict the key by IP allowlist when your servers have stable egress IPs.
    • Turn on required request signing (HMAC) for keys used outside a locked network.
    • On 429, respect Retry-After if present; on 402, check billing or spending cap before retrying.

    Body parameters · Security · Errors

Downloads

POST /generate

Reference for POST /v1/generate: auth, URL, headers, and top-level body. Full curl/PHP/JS/Python samples are in Guide.

Authorizations

ApiKeyAuth

Send the secret in X-API-Key on every request. If the key requires signing, also send X-WebQR-Timestamp and X-WebQR-Signature.

Request method

POST

Request URL

https://api.webqr.io/v1/generate

Headers

Parameter Type Description
X-API-Key Required
string Your API key secret.
Content-Type Required
string Send application/json so the body is parsed as JSON. Missing it often surfaces as validation errors on data.
X-WebQR-Timestamp
string Unix timestamp (seconds). Required when the key enforces signing.
X-WebQR-Signature
string HMAC-SHA256 hex of the canonical string. Required when the key enforces signing.

Parameters

Parameter Type Description
data Required
string Payload to encode (URL, text, Wi‑Fi string, etc.). Max 1000 characters.
design
object Optional design object. Omitted fields use defaults.

Full design fields (styleType, eyes, frames, logos, gradients) are listed under Parameters.

Body parameters · Full request examples in Guide

Successful response (HTTP 200)

JSON: success, SVG in data.qr_code (format svg), echoed content, normalized design, and a full usage object (counters, reset time, human-readable hints).

{
    "success": true,
    "data": {
        "qr_code": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 100 100\">…</svg>",
        "format": "svg",
        "content": "https://webqr.io",
        "design": {
            "size": 512,
            "color": "#000000",
            "backgroundColor": "#FFFFFF",
            "styleType": "a7k2m9",
            "eyeType": "r2s4t6",
            "showColorGradient": false
        }
    },
    "usage": {
        "period": "2026-05",
        "used_count": 42,
        "included_limit": 200,
        "paid_count": 0,
        "charged_cents": 0,
        "spending_cap_cents": 1000,
        "included_quota_resets_at": "2026-06-01T00:00:00+00:00",
        "what_counts_toward_limit": "Successful POST /v1/generate that returns SVG.",
        "what_does_not_count": "GET /v1/usage, validation errors, and failed generations.",
        "included_quota_note": "Included quota resets at the start of each calendar month.",
        "overage_linear_floor_hint": "Overage is billed per 1000 paid generations (linear floor)."
    }
}

Usage

Read-only: period counters; `data.items` is empty. Does not use generation quota.

Authorizations

ApiKeyAuth

Send the secret in X-API-Key on every request. If the key requires signing, also send X-WebQR-Timestamp and X-WebQR-Signature.

Request method

GET

Request URL

https://api.webqr.io/v1/usage

Headers

Parameter Type Description
X-API-Key Required
string Your API key secret.
X-WebQR-Timestamp
string Unix timestamp (seconds). Required when the key enforces signing.
X-WebQR-Signature
string HMAC-SHA256 hex of the canonical string. Required when the key enforces signing.

If the key requires signed requests, include X-WebQR-Timestamp and X-WebQR-Signature on GET /v1/usage as well (empty body → SHA-256 of "").

Parameters

This endpoint has no query or body parameters. Authentication is via headers only.

Example request

curl -X GET https://api.webqr.io/v1/usage \
  -H "X-API-Key: wq_xxxxxxxxxx_your_secret"
<?php
$ch = curl_init('https://api.webqr.io/v1/usage');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER => [
    'X-API-Key: wq_xxxxxxxxxx_your_secret',
  ],
]);
echo curl_exec($ch);
const res = await fetch('https://api.webqr.io/v1/usage', {
  headers: {
    'X-API-Key': 'wq_xxxxxxxxxx_your_secret',
  },
});
const data = await res.json();
console.log(data);
import requests

r = requests.get(
  "https://api.webqr.io/v1/usage",
  headers={
    "X-API-Key": "wq_xxxxxxxxxx_your_secret",
  },
)
print(r.json())

Returns the current usage snapshot for the account behind the key. Does not consume generation quota. The `usage` object matches the block returned by POST /v1/generate.

Body parameters

Request body for POST /v1/generate: required data, optional design. Use the codes from Modules, Eyes, Frames, and Logos below.

Parameter Type Description
data Required
string Payload to encode (URL, text, Wi‑Fi string, etc.). Max 1000 characters.
design
object Optional design object. Omitted fields use defaults.
design.size
integer Output size in pixels (100–2048).
design.color
string Module (dot) color as #RRGGBB.
design.backgroundColor
string Background color as #RRGGBB.
design.borderColor
string Outer finder (eye) color as #RRGGBB.
design.centerColor
string Inner finder (eye) color as #RRGGBB.
design.markerOutColor
string Alias of borderColor.
design.markerInColor
string Alias of centerColor.
design.styleType
string Module shape code (see Module codes).
design.eyeType
string Outer eye shape code (see Outer eyes).
design.eyeInnerType
string Inner eye shape code (see Inner eyes).
design.showColorGradient
boolean Enable gradient on modules.
design.unifiedGradient
boolean Apply one gradient across modules and finders.
design.showAllColorGradient
boolean Alias of unifiedGradient.
design.showEyeGradient
boolean Enable gradient on finders.
design.styleColorGradient
string Module gradient direction.
design.eyeGradientStyle
string Finder gradient direction.
design.fromColor
string Module gradient start #RRGGBB.
design.toColor
string Module gradient end #RRGGBB.
design.eyeFromColor
string Finder gradient start #RRGGBB.
design.eyeToColor
string Finder gradient end #RRGGBB.
design.transparent
boolean Transparent background (no fill).
design.title
string Optional title metadata (max 100).
design.frameType
string Frame code from Frames. Omit the field or leave it empty when you do not want a frame.
design.frameColor
string Frame color as #RRGGBB.
design.showFrameGradient
boolean Enable frame gradient.
design.frameGradientFrom
string Frame gradient start #RRGGBB.
design.frameGradientTo
string Frame gradient end #RRGGBB.
design.frameGradientStyle
string Frame gradient direction.
design.frameGradientTextColor
string Caption color when frame gradient is on.
design.textColor
string Caption / frame text color as #RRGGBB.
design.textUnderQr
string Caption under the QR (max 40; used with frames).
design.roundedCorners
boolean Round the QR canvas corners.
design.cornerRadius
integer Corner radius in px (0–120) when roundedCorners is true.
design.logo
string Recommended: preset code (Lxxxxx) or a public HTTPS image URL. Advanced: data URIs and some internal storage paths may work; prefer HTTPS for integrations.
design.selectedLogo
string Preset logo code or HTTPS image URL.
design.logoBackgroundEnabled
boolean Draw a backing shape behind the logo.
design.loadLogoBackgroundOut
string Logo backing / outer color as #RRGGBB (Lxxxxx presets).
design.loadLogoBackgroundIn
string Logo icon / inner color as #RRGGBB (Lxxxxx presets).

Response format is SVG only (data.format: "svg"). PNG/PDF are not available through this API.

Module codes (design.styleType)

Use the code string in design.styleType, eyeType, eyeInnerType, frameType, or logo.

  • a7k2m9 Square
  • b3n5p1 Square indent
  • c8q4r6 Squircle
  • d1s7t9 Rounded connected
  • e6u2v4 Sharp connected
  • f9w5x7 Dot
  • g2y8z0 Rounded dot
  • h4a6b8 Dot (horizontal)
  • i0c2d4 Dot (vertical)
  • j6e8f0 Diamond
  • k2g4h6 Star
  • l8i0j2 Star pixel
  • m4k6l8 Heart
  • n0o2p4 Plus
  • p6q8r0 Plus rounded
  • q1r3s5 Square round
  • s1r3s5 Square round

Outer eye shapes (design.eyeType)

Use the code string in design.styleType, eyeType, eyeInnerType, frameType, or logo.

  • r2s4t6 Square
  • u8v0w2 Rounded square
  • x4y6z8 Circle
  • a1b3c5 Diamond
  • d7e9f1 D-shape
  • g3h5i7 D-shape inverted
  • j9k1l3 Leaf
  • m5n7o9 Leaf circle
  • p1q3r5 Leaf circle rotated
  • s7t9u1 Leaf variant
  • v3w5x7 Square circle
  • y9z1a3 Rounded pointed
  • b5c7d9 Squircle
  • e1f3g5 Droplet

Inner eye shapes (design.eyeInnerType)

Use the code string in design.styleType, eyeType, eyeInnerType, frameType, or logo.

  • h7i9j1 Square
  • k3l5m7 Squircle
  • n9o1p3 Circle
  • q5r7s9 Skewed square
  • t1u3v5 D-shape
  • w7x9y1 D-shape inverted
  • z3a5b7 Leaf
  • c9d1e3 Star
  • f5g7h9 Diamond
  • i1j3k5 X shape
  • l7m9n1 Plus
  • o3p5q7 Clover
  • r9s1t3 Rounded X
  • u5v7w9 Heart

Frames (design.frameType)

n0f1r2 = no frame (same as omitting frameType or an empty string). t7r8d9 = thick rounded border with no caption. Other frames may use a caption strip; optional textUnderQr overrides it; otherwise the server applies default wording.

  • n0f1r2 No frame
  • t7r8d9 Thick rounded frame
  • l4b5t6 Label bottom
  • b3n4t5 Banner top
  • b7b8d9 Badge bottom
  • b5d6t7 Badge top
  • b9b0t1 Bar bottom
  • b2t3p4 Bar top
  • w4t5p6 Wide bar top
  • w1d2b3 Wide bar bottom
  • p0l1b2 Pill bottom
  • p8t9p0 Pill top
  • s6b7g8 Shopping bag

Preset logos (design.logo)

Same presets as in the builder: send a code (La1b2c…) in design.logo or design.selectedLogo. For presets use loadLogoBackgroundOut and loadLogoBackgroundIn (#RRGGBB). Also https URLs and data:image/…; logoBackgroundEnabled for custom logos. See docs/qr-logos.md in the repo.

  • La1b2c Facebook
  • Ld3e4f Messenger
  • Lg5h6i Instagram
  • Lj7k8l LinkedIn
  • Lm9n0o YouTube
  • Ls3t4u Phone
  • Lv5w6x Telegram
  • Ly7z8a SMS
  • Lb9c0d Email
  • Le1f2g Wi‑Fi
  • Lh3i4j Restaurant
  • Lk5l6m Location
  • Ln7o8p Gallery
  • Lq9r0s Profile
  • Lt1u2v WhatsApp
  • Lw3x4y Link
  • Lz5a6b Promo
  • Lc7d8e Lock

Logo fields

logo and selectedLogo are aliases. loadLogoBackground* applies to Lxxxxx presets only.

  • design.logo
  • design.selectedLogo
  • design.loadLogoBackgroundOut
  • design.loadLogoBackgroundIn
  • design.logoBackgroundEnabled

Gradient directions

Use these strings for styleColorGradient or eyeGradientStyle.

  • horizontal
  • vertical
  • diagonal
  • inverse_diagonal
  • radial

API limits

Included monthly generations, active keys, and rate limits by plan. Product pricing is on the Pricing page.

By plan

Included successful generations per calendar month, active keys, and request rate by plan.

Plan Included / month Active keys Rate limit
Starter 200 generations up to 1 30/min · burst 2/s
Premium 5000 generations up to 5 120/min · burst 8/s
Business 25000 generations up to 15 300/min · burst 15/s
Enterprise 50000 generations up to 30 600/min · burst 25/s

Need more volume or a custom plan? Contact us — we’ll help with individual limits and terms.

Quota resets at the start of each calendar month. Per-key rate limits apply after authentication. Pre-auth (missing/invalid key) is limited per IP.

What counts and what does not

  • Only a successful POST /v1/generate that returns SVG counts as +1 toward used_count.
  • Included allowance resets every calendar month; unused generations do not roll over.
  • GET /v1/usage never spends quota — use it for dashboards and alerts.
  • Authenticated: per key (per minute + short burst). Pre-auth / invalid key: about 45 requests per minute per IP.
  • The API does not archive QR codes into your WebQR library. Persist SVG and your own ids yourself.
  • Successful responses return SVG only — there is no PNG/PDF endpoint on Static QR API.
  • After included quota, paid overage is about $2 per 1000 generations when enabled in the cabinet. Default spending cap is about $10/month (configurable, hard max applies).

Related

  • Quota Details on what increments used_count and how the usage object is shaped.
  • API billing in cabinet Enable overage and set a monthly spend cap in the cabinet.
  • Plans and pricing Product plan prices and what each plan includes.

How quota is counted

Only a successful POST /v1/generate with SVG increments used_count. GET /v1/usage never spends quota. Field list below matches the usage object in responses.

  • One successful generate = +1 used_count. Validation errors and 4xx/5xx do not count.
  • GET /v1/usage is for dashboards and alerts — same counters, zero spend.
  • If overage billing is connected and status is payment_failed, all Static QR API calls are blocked (including included quota) until payment is resolved.
  • Localized hint strings (what_counts_toward_limit, included_quota_note, …) are for humans; automate on numeric fields and error.code.

Fields present on the `usage` object in generate and usage responses:

  • period
  • used_count
  • included_limit
  • paid_count
  • charged_cents
  • spending_cap_cents
  • included_quota_resets_at
  • what_counts_toward_limit
  • what_does_not_count
  • included_quota_note
  • overage_linear_floor_hint

API billing in cabinet

Typical HTTP errors

HTTP status alone is not enough — JSON shapes differ by layer. Gateway auth (missing/invalid key, IP) often returns `{ "error": "…" }`. Quota and billing blocks return `{ "success": false, "error": { "code", "message" } }`. Validation is Laravel-style `{ "errors": {…} }` (often with `message`). Server failures may use `{ "success": false, "message" }` without `error.code`. Message language may be English or Russian depending on the layer — do not rely on locale for machine parsing; use status + `error.code` when present.

HTTP error.code Meaning
401 No X-API-Key header.
401 Malformed, unknown, revoked, or expired API key.
401 Missing/invalid signature when signing headers were sent, or invalid/expired signature when the key requires signing.
403 Client IP is not on this key’s allowlist.
402 billing_payment_required API overage billing is in payment_failed — the whole Static QR API is blocked, including included quota, until payment is fixed.
402 free_limit_reached Monthly included quota is exhausted and overage billing is not connected.
402 spending_cap_reached Monthly spending cap for overage is reached.
422 Validation failed (types, unknown style codes, invalid HEX, oversized fields, rejected logo URL, etc.).
429 rate_limited Too many requests from this IP (pre-auth) or key (post-auth). Retry later; respect Retry-After when present.
500 Unexpected generation failure — retry later.

JSON shapes by status

Examples of typical bodies. Field wording can vary; treat `error.code` as the stable signal when it exists.

{
    "error": "Invalid API key."
}
{
    "success": false,
    "error": {
        "code": "billing_payment_required",
        "message": "API billing payment failed or is overdue."
    }
}
{
    "message": "The data field is required.",
    "errors": {
        "data": [
            "The data field is required."
        ]
    }
}
{
    "success": false,
    "error": {
        "code": "rate_limited",
        "message": "Too many requests. Try again later."
    }
}
{
    "success": false,
    "message": "QR code generation failed. Please try again later."
}

Production security

Treat the API key like a password for your WebQR account quota.

  • Keep the API key on your server only — never in browsers, mobile apps, or public repos.
  • Call api.* from your backend. Browser CORS is not intended for Static QR API — use your server or the API Builder in the cabinet.
  • Optionally restrict each key to your server IPs (or fixed CDN egress). Empty allowlist = any IP; empty array of rules can block all.
  • Optionally require HMAC signing per key in the cabinet. When enabled, every route needs X-WebQR-Timestamp and X-WebQR-Signature — including GET /v1/usage.

HMAC request signing (when required for the key)

Signing string (UTF-8)

1714412345
POST
/v1/generate
<sha256_hex_of_raw_body>

For GET requests the raw body is empty. Use SHA-256 of an empty string as the last line:

1714412345
GET
/v1/usage
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

HMAC secret: 40 characters after the second underscore. Signature is lowercase hex. Timestamp: Unix seconds, skew within ±5 minutes. Body SHA-256 must match the exact request bytes. The path line must match the request URL path (e.g. /v1/generate or /v1/usage).

Compute the signature

Secret = 40 characters after the second underscore in the key (same secret as X-API-Key). Signature = lowercase hex HMAC-SHA256 of the canonical string. Clock skew allowed: ±5 minutes.

# Build signature on your server, then attach headers to curl
TS=$(date +%s)
BODY='{"data":"https://webqr.io"}'
BODY_HASH=$(printf %s "$BODY" | openssl dgst -sha256 -hex | awk '{print $2}')
SECRET="${API_KEY##*_}"  # 40 chars after the second underscore
CANON=$(printf '%s\nPOST\n/v1/generate\n%s' "$TS" "$BODY_HASH")
SIG=$(printf %s "$CANON" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $2}')
curl -X POST https://api.webqr.io/v1/generate \
  -H "Content-Type: application/json" \
  -H "X-API-Key: $API_KEY" \
  -H "X-WebQR-Timestamp: $TS" \
  -H "X-WebQR-Signature: $SIG" \
  -d "$BODY"
<?php
$apiKey = getenv('WEBQR_API_KEY');
$secret = substr($apiKey, strrpos($apiKey, '_') + 1);
$ts = (string) time();
$body = json_encode(['data' => 'https://webqr.io'], JSON_UNESCAPED_SLASHES);
$canonical = $ts . "\nPOST\n/v1/generate\n" . hash('sha256', $body);
$signature = hash_hmac('sha256', $canonical, $secret);

$ch = curl_init('https://api.webqr.io/v1/generate');
curl_setopt_array($ch, [
  CURLOPT_POST => true,
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER => [
    'Content-Type: application/json',
    'X-API-Key: ' . $apiKey,
    'X-WebQR-Timestamp: ' . $ts,
    'X-WebQR-Signature: ' . $signature,
  ],
  CURLOPT_POSTFIELDS => $body,
]);
echo curl_exec($ch);
import crypto from 'node:crypto';

const apiKey = process.env.WEBQR_API_KEY;
const secret = apiKey.slice(apiKey.lastIndexOf('_') + 1);
const ts = String(Math.floor(Date.now() / 1000));
const body = JSON.stringify({ data: 'https://webqr.io' });
const bodyHash = crypto.createHash('sha256').update(body).digest('hex');
const canonical = `${ts}\nPOST\n/v1/generate\n${bodyHash}`;
const signature = crypto.createHmac('sha256', secret).update(canonical).digest('hex');

const res = await fetch('https://api.webqr.io/v1/generate', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-API-Key': apiKey,
    'X-WebQR-Timestamp': ts,
    'X-WebQR-Signature': signature,
  },
  body,
});
console.log(await res.json());
import hashlib, hmac, json, os, time, urllib.request

api_key = os.environ["WEBQR_API_KEY"]
secret = api_key.rsplit("_", 1)[-1]
ts = str(int(time.time()))
body = json.dumps({"data": "https://webqr.io"}, separators=(",", ":"))
body_hash = hashlib.sha256(body.encode()).hexdigest()
canonical = f"{ts}\nPOST\n/v1/generate\n{body_hash}"
signature = hmac.new(secret.encode(), canonical.encode(), hashlib.sha256).hexdigest()

req = urllib.request.Request("https://api.webqr.io/v1/generate", data=body.encode(), method="POST")
req.add_header("Content-Type", "application/json")
req.add_header("X-API-Key", api_key)
req.add_header("X-WebQR-Timestamp", ts)
req.add_header("X-WebQR-Signature", signature)
print(urllib.request.urlopen(req).read().decode())

Request shape with signing headers

curl -X POST https://api.webqr.io/v1/generate \
  -H "Content-Type: application/json" \
  -H "X-API-Key: wq_xxxxxxxxxx_your_secret" \
  -H "X-WebQR-Timestamp: 1714412345" \
  -H "X-WebQR-Signature: 64_hex_chars_lowercase_hmac_sha256_placeholder_do_not_use_as_is" \
  -d '{
    "data": "https://webqr.io",
    "design": {
        "size": 512,
        "color": "#000000",
        "backgroundColor": "#FFFFFF",
        "styleType": "a7k2m9",
        "eyeType": "r2s4t6",
        "showColorGradient": false
    }
}'
<?php
$ch = curl_init('https://api.webqr.io/v1/generate');
curl_setopt_array($ch, [
  CURLOPT_POST => true,
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER => [
    'Content-Type: application/json',
    'X-API-Key: wq_xxxxxxxxxx_your_secret',
    'X-WebQR-Timestamp: 1714412345',
    'X-WebQR-Signature: 64_hex_chars_lowercase_hmac_sha256_placeholder_do_not_use_as_is'
  ],
  CURLOPT_POSTFIELDS => '{
    "data": "https://webqr.io",
    "design": {
        "size": 512,
        "color": "#000000",
        "backgroundColor": "#FFFFFF",
        "styleType": "a7k2m9",
        "eyeType": "r2s4t6",
        "showColorGradient": false
    }
}',
]);
echo curl_exec($ch);
const res = await fetch('https://api.webqr.io/v1/generate', {
  method: 'POST',
  headers: {
      "Content-Type": "application/json",
      "X-API-Key": "wq_xxxxxxxxxx_your_secret",
      "X-WebQR-Timestamp": "1714412345",
      "X-WebQR-Signature": "64_hex_chars_lowercase_hmac_sha256_placeholder_do_not_use_as_is"
  },
  body: JSON.stringify({
    "data": "https://webqr.io",
    "design": {
        "size": 512,
        "color": "#000000",
        "backgroundColor": "#FFFFFF",
        "styleType": "a7k2m9",
        "eyeType": "r2s4t6",
        "showColorGradient": false
    }
}),
});
const data = await res.json();
console.log(data);
import requests

r = requests.post(
  "https://api.webqr.io/v1/generate",
  headers={
      "Content-Type": "application/json",
      "X-API-Key": "wq_xxxxxxxxxx_your_secret",
      "X-WebQR-Timestamp": "1714412345",
      "X-WebQR-Signature": "64_hex_chars_lowercase_hmac_sha256_placeholder_do_not_use_as_is"
  },
  json={
      "data": "https://webqr.io",
      "design": {
          "size": 512,
          "color": "#000000",
          "backgroundColor": "#FFFFFF",
          "styleType": "a7k2m9",
          "eyeType": "r2s4t6",
          "showColorGradient": false
      }
  },
)
print(r.json())

With “Require signed requests” on the key, both signing headers are mandatory on every request. Sending only one header is rejected.

Frequently asked questions

Can I use the API on the free plan?

Yes. Starter already includes Static QR API with 200 successful generations per calendar month; Premium 5000; Business 25000. No separate API-only subscription. Optional paid overage is available in the cabinet.

How many generations do I get per month?

Included successful generations per calendar month: Starter 200, Premium 5000, Business 25000, Enterprise 50000. Unused generations do not roll over. Details are in the Limits section on this page.

Where do I create an API key?

After sign-up, open API keys in the cabinet, create a key, and copy the secret once (it is shown only at creation). Use it only on your backend — never in browser JavaScript or a mobile app binary.

Can I call the API from the browser or a mobile app?

No. The key must stay on your server. Call POST /v1/generate from your backend, then send the SVG (or a URL to it) to the client.

What does a successful response contain? Does WebQR store every QR?

You get JSON with the SVG in data.qr_code, echo of content/design, and a usage object for quota. The API is stateless: WebQR does not save each call into your library or return a long-lived id — keep the SVG (and any ids) on your side.

Which formats does the API return?

SVG only (data.format is "svg"). There is no PNG/PDF endpoint on Static QR API — convert on your side if you need other formats.

What happens when I exceed the included quota?

Enable API billing in the cabinet. Overage is about $2 per 1,000 successful generations, with a monthly spending cap (default about $10). Without billing enabled, requests that need overage are rejected.

Can I use both the API and the on-site generator?

Yes. The API is for server integrations; the main generator and cabinet are for interactive work. They share design codes, but API calls do not auto-create library items.

Ready to generate QR from your server?

Sign up, create an API key in the cabinet, then follow the guide on this page.

Starter — free forever No credit card Cancel anytime