CH Layer docs

API docs for Swiss infrastructure workflows.

Create an account, generate a test API key, send your first QR-bill request, then activate billing before live production traffic.

Base URL

https://api.ch-layer.ch

Authentication

Send API keys as bearer tokens: Authorization: Bearer sk_test_... or Authorization: Bearer sk_live_.... API keys are scoped to one dashboard account.

Test and live mode

Test keys include X-Test-Mode: true and are meant for onboarding/sandbox use. Live keys are for production traffic and require active billing for paid plans.

Plan enforcement

Live API traffic for Starter, Growth, and Scale requires billing status active or trialing. Inactive, past-due, or canceled billing returns payment_required. Test keys remain available so developers can fix integration issues.

Rate limits

Responses include X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset. Free: 10/min; Starter: 60/min; Growth: 200/min; Scale: 1000/min. Monthly limits are enforced per account and environment.

Idempotency

For create/render requests, send Idempotency-Key with a stable unique value such as your invoice ID. Replays return the original response instead of duplicating work.

QR-bill validation schema

The first production module validates and renders Swiss QR-bill payloads. Address fields use building_number and town.

{
  "iban": "CH9300762011623852957",
  "amount": 199.95,
  "currency": "CHF",
  "reference_type": "NON | SCOR | QRR",
  "reference": null,
  "creditor": {
    "name": "Acme AG",
    "street": "Musterstrasse",
    "building_number": "1",
    "postal_code": "8000",
    "town": "Zürich",
    "country": "CH"
  },
  "debtor": {
    "name": "Customer AG",
    "street": "Bahnhofstrasse",
    "building_number": "10",
    "postal_code": "3000",
    "town": "Bern",
    "country": "CH"
  },
  "unstructured_message": "Optional payment message"
}

Valid QRR example

Use QRR only with a QR-IBAN and a valid 27-digit QR reference.

{
  "iban": "CH4431999123000889012",
  "amount": 249.0,
  "currency": "CHF",
  "reference_type": "QRR",
  "reference": "210000000003139471430009017",
  "creditor": {
    "name": "Acme AG",
    "street": "Musterstrasse",
    "building_number": "1",
    "postal_code": "8000",
    "town": "Zürich",
    "country": "CH"
  },
  "unstructured_message": "Invoice 2026-0001"
}

Endpoints

POST /v1/qr-bills
POST /v1/qr-bills/validate
POST /v1/qr-bills/render
POST /v1/qr-bills/parse
POST /v1/qr-bills/batch

Render PDF/SVG/PNG

Render calls use one endpoint and select svg, png, or pdf in the JSON body.

curl https://api.ch-layer.ch/v1/qr-bills/render \
  -H "Authorization: Bearer sk_test_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: invoice-2026-0001-pdf" \
  -d '{
    "format": "pdf",
    "qr_bill": {
      "iban": "CH9300762011623852957",
      "amount": 199.95,
      "currency": "CHF",
      "reference_type": "NON",
      "reference": null,
      "creditor": {
        "name": "Acme AG",
        "street": "Musterstrasse",
        "building_number": "1",
        "postal_code": "8000",
        "town": "Zürich",
        "country": "CH"
      }
    },
    "options": { "language": "en" }
  }' \
  --output qr-bill.pdf

Render response formats

// Render output is selected by JSON body, not by path suffix:
{ "format": "svg", "qr_bill": { ... } }
{ "format": "png", "qr_bill": { ... } }
{ "format": "pdf", "qr_bill": { ... } }

// Successful render responses are downloadable files:
Content-Type: image/svg+xml | image/png | application/pdf
Content-Disposition: attachment; filename=qr-bill.svg|png|pdf

curl · SVG

curl https://api.ch-layer.ch/v1/qr-bills/render \
  -H "Authorization: Bearer sk_test_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: invoice-2026-0001-svg" \
  -d '{
    "format": "svg",
    "qr_bill": {
      "iban": "CH9300762011623852957",
      "creditor": {
        "name": "Acme AG",
        "street": "Musterstrasse",
        "building_number": "1",
        "postal_code": "8000",
        "town": "Zürich",
        "country": "CH"
      },
      "amount": 199.95,
      "currency": "CHF",
      "reference_type": "NON",
      "reference": null,
      "unstructured_message": "Invoice 2026-0001"
    }
  }' \
  --output qr-bill.svg

JavaScript · PNG

const response = await fetch("https://api.ch-layer.ch/v1/qr-bills/render", {
  method: "POST",
  headers: {
    "Authorization": "Bearer sk_test_...",
    "Content-Type": "application/json",
    "Idempotency-Key": "invoice-2026-0001-png"
  },
  body: JSON.stringify({
    format: "png",
    qr_bill: {
      "iban": "CH9300762011623852957",
      "creditor": {
        "name": "Acme AG",
        "street": "Musterstrasse",
        "building_number": "1",
        "postal_code": "8000",
        "town": "Zürich",
        "country": "CH"
      },
      "amount": 199.95,
      "currency": "CHF",
      "reference_type": "NON",
      "reference": null,
      "unstructured_message": "Invoice 2026-0001"
    }
  })
});

if (!response.ok) throw new Error(await response.text());
const file = await response.blob();

Python · PDF

import json
import requests

response = requests.post(
    "https://api.ch-layer.ch/v1/qr-bills/render",
    headers={
        "Authorization": "Bearer sk_test_...",
        "Content-Type": "application/json",
        "Idempotency-Key": "invoice-2026-0001-pdf",
    },
    json={
        "format": "pdf",
        "qr_bill": json.loads("{\n  \"iban\": \"CH9300762011623852957\",\n  \"creditor\": {\n    \"name\": \"Acme AG\",\n    \"street\": \"Musterstrasse\",\n    \"building_number\": \"1\",\n    \"postal_code\": \"8000\",\n    \"town\": \"Zürich\",\n    \"country\": \"CH\"\n  },\n  \"amount\": 199.95,\n  \"currency\": \"CHF\",\n  \"reference_type\": \"NON\",\n  \"reference\": null,\n  \"unstructured_message\": \"Invoice 2026-0001\"\n}"),
    },
)
response.raise_for_status()
with open("qr-bill.pdf", "wb") as output:
    output.write(response.content)

Error responses

Errors use a stable envelope with error.type and error.message. Common types: authentication_error, validation_error, rate_limit_exceeded, payment_required, and configuration_error.

{
  "error": {
    "type": "payment_required",
    "message": "Live API access requires an active subscription."
  }
}