---
name: bezbrany-payments
description: >-
  Integrace BezBrany — české QR platby bankovním převodem (SPAYD) bez platební
  brány. Použij při napojování e-shopu, SaaS nebo aplikace na BezBrany API
  (api.bezbrany.cz): vytváření plateb, vložení platebního widgetu, hosted
  checkout, ověřování webhooků. Czech QR bank payment integration (BezBrany).
---

# BezBrany — integrace plateb

BezBrany je česká platební platforma pro QR platby bankovním převodem (formát SPAYD).
Zákazník naskenuje QR kód v bankovní aplikaci, převod dorazí přímo na účet obchodníka
(Fio banka) a BezBrany platbu automaticky spáruje podle variabilního symbolu. Žádná
karetní brána, žádné procento z transakce.

## Základní fakta

|                   |                                                                           |
| ----------------- | ------------------------------------------------------------------------- |
| API base URL      | `https://api.bezbrany.cz`                                                 |
| Web + dashboard   | `https://bezbrany.cz`                                                     |
| OpenAPI (Swagger) | `https://api.bezbrany.cz/api/v1/docs` (JSON: `/api/v1/docs/openapi.json`) |
| Měna              | CZK                                                                       |
| **Částky**        | **v korunách jako decimal číslo** (`100` = 100,00 Kč) — NE v haléřích     |

## API klíče a autentizace

| Prefix      | Hlavička       | Kde smí být          | Oprávnění                            |
| ----------- | -------------- | -------------------- | ------------------------------------ |
| `sk_live_…` | `X-API-Key`    | jen server (backend) | plný přístup k Public API            |
| `sk_test_…` | `X-API-Key`    | jen server (backend) | testovací platby + simulace úspěchu  |
| `pk_…`      | `X-Public-Key` | frontend/prohlížeč   | jen vytvoření platby přes widget API |

**Nikdy nevkládej `sk_` klíč do frontendového kódu.** Pro platby vytvářené přímo
z prohlížeče použij veřejný `pk_` klíč a widget API. Klíče se generují v dashboardu:
`https://bezbrany.cz/nastenka/nastaveni/api-klice`.

## Předpoklady (jednorázové nastavení obchodníka)

1. Registrace na `https://bezbrany.cz/prihlaseni` (magic link nebo Google).
2. Onboarding v dashboardu: údaje o firmě + bankovní účet s Fio API tokenem (read-only
   token z internetbankingu Fio). Nový Fio token vyžaduje jednorázové potvrzení v bance (PSD2).
3. Vygenerovat API klíč (test i live) v Nastavení → API klíče.
4. Pro widget na vlastní doméně: přidat doménu v Nastavení → CORS domény
   (`https://bezbrany.cz/nastenka/nastaveni/cors`, max 100 domén, změna se projeví do 60 s).

## Tři způsoby integrace — kdy který

1. **REST API (backend)** — plná kontrola; e-shop se serverovou částí. Vytvoření platby
   ze serveru s `sk_` klíčem, zobrazení QR/widgetu, potvrzení webhookem.
2. **JS widget (frontend)** — nejrychlejší; statické weby nebo jednoduché košíky.
   Platbu lze vytvořit i přímo z prohlížeče s `pk_` klíčem.
3. **Hosted checkout (bez kódu)** — přesměrování zákazníka na
   `https://bezbrany.cz/platba/{paymentCode}`; stránku hostuje BezBrany.

## REST API

### Vytvoření platby

```bash
curl -X POST https://api.bezbrany.cz/api/v1/payments \
  -H "X-API-Key: sk_live_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: obj-2026-001-pokus-1" \
  -d '{
    "amount": 1250.50,
    "currency": "CZK",
    "orderNumber": "OBJ-2026-001",
    "orderDescription": "Nákup v e-shopu",
    "payer": {
      "contact": { "firstName": "Jan", "lastName": "Novák", "email": "jan@example.cz", "phone": "+420777123456" },
      "billingAddress": { "street": "Vodičkova 12", "city": "Praha 1", "zipCode": "11000", "countryCode": "CZE" }
    },
    "items": [
      { "name": "Tričko s potiskem", "amount": 1250.50, "quantity": 1, "vatRate": 21 }
    ],
    "callback": {
      "returnUrl": "https://vas-eshop.cz/objednavka/dekujeme",
      "notificationUrl": "https://vas-eshop.cz/api/bezbrany-webhook"
    },
    "metadata": { "source": "web" }
  }'
```

- Povinné: `amount`, `currency`, `orderNumber`, `payer.contact` (firstName, lastName, email), `items` (name, amount, quantity).
- `amount` musí být v rozsahu **1 – 10 000 000 Kč**; `currency` je zatím jen `CZK`.
- `items[].amount` je cena za kus v korunách; součet položek by měl odpovídat `amount`.
- `callback.returnUrl` i `callback.notificationUrl` musí být **https** na veřejnou doménu
  (http, `localhost` a interní/privátní IP jsou v produkci odmítnuty — SSRF ochrana).
- `Idempotency-Key` (volitelná hlavička) chrání proti duplicitám při retry — stejný klíč
  se stejným payloadem vrátí původní odpověď, s jiným payloadem vrátí konflikt.

Odpověď `201`:

```json
{
	"paymentCode": "7E200D6C96FB4118",
	"status": "pending",
	"amount": 1250.5,
	"currency": "CZK",
	"orderNumber": "OBJ-2026-001",
	"qrCodeData": "SPD*1.0*ACC:CZ..*AM:1250.50*CC:CZK*X-VS:..."
}
```

`qrCodeData` je SPAYD řetězec — vykresli ho jako QR kód, nebo použij widget/hosted
checkout, které QR zobrazí samy.

### Stav platby

```bash
curl https://api.bezbrany.cz/api/v1/payments/{paymentCode} -H "X-API-Key: sk_live_..."
```

Stavy: `pending` → `completed` | `expired` | `failed`; po refundaci `refunded` /
`partially_refunded`. Platba expiruje za 24 h. Pro potvrzení objednávky se spoléhej
primárně na webhook, stav plánovaně nepolluj častěji než ~1× za 5 s.

### Testování

S `sk_test_` klíčem vytvoř platbu a dokonči ji bez skutečných peněz:

```bash
curl -X POST https://api.bezbrany.cz/api/v1/payments/{paymentCode}/simulate-success \
  -H "X-API-Key: sk_test_..."
```

Simulace spustí i webhook — otestuješ tím celý flow.

## JS widget

Widget zobrazí QR kód, sleduje stav platby v reálném čase (Socket.IO) a po zaplacení
zobrazí potvrzení. Doména, kde widget běží, musí být v CORS doménách obchodníka.

```html
<script src="https://api.bezbrany.cz/widget/v1.js"></script>
<div data-bezbrany-widget data-payment-code="PAYMENT_CODE_Z_API"></div>
```

Programaticky:

```html
<script src="https://api.bezbrany.cz/widget/v1.js"></script>
<div id="platba"></div>
<script>
	BezBranyWidget.init({
		paymentCode: 'PAYMENT_CODE_Z_API',
		containerId: 'platba',
		onPaymentSuccess: () => {
			window.location.href = '/dekujeme';
		},
	});
</script>
```

Vytvoření platby přímo z prohlížeče (bez vlastního backendu) — veřejný `pk_` klíč:

```js
const res = await fetch('https://api.bezbrany.cz/api/v1/widget/payments', {
	method: 'POST',
	headers: { 'Content-Type': 'application/json', 'X-Public-Key': 'pk_...' },
	body: JSON.stringify({
		amount: 1250.5,
		currency: 'CZK',
		orderNumber: 'OBJ-2026-001',
		payer: { contact: { firstName: 'Jan', lastName: 'Novák', email: 'jan@example.cz' } },
		items: [{ name: 'Tričko', amount: 1250.5, quantity: 1 }],
	}),
});
const { paymentCode } = await res.json();
```

Stav pro widget (bez autentizace): `GET https://api.bezbrany.cz/api/v1/widget/payments/{paymentCode}`.

## Webhooky

Po dokončení platby pošle BezBrany `POST` na `callback.notificationUrl` (per platba)
nebo na webhook URL z profilu obchodníka:

```json
{
	"id": "b7c3e1a2-5f9d-4c2a-8e11-9a0f2d3c4b5e",
	"event": "payment.completed",
	"createdAt": "2026-07-24T10:12:30.000Z",
	"paymentId": "uuid",
	"paymentCode": "7E200D6C96FB4118",
	"orderNumber": "OBJ-2026-001",
	"status": "completed",
	"amount": 1250.5,
	"currency": "CZK",
	"metadata": {}
}
```

- `id` je unikátní ID doručení → **deduplikuj podle něj** (retry pošle stejné `id`).
- `event` je typ události (dnes `payment.completed`) → **filtruj podle něj** a ignoruj
  neznámé typy, ať tě nerozbijí budoucí události.
- `createdAt` je čas vzniku události (ISO 8601).

Každý webhook nese hlavičku `X-BezBrany-Signature: t=<unix_timestamp>,v1=<hex_podpis>`.
Podpis je HMAC SHA-256 z řetězce `"<t>.<raw_body>"` webhook secretem obchodníka —
**ověřuj nad raw tělem requestu, ne nad re-serializovaným JSON**:

```js
const crypto = require('crypto');

function verifyWebhook(rawBody, signatureHeader, secret) {
	const parts = Object.fromEntries(signatureHeader.split(',').map((p) => p.split('=')));
	const expected = crypto
		.createHmac('sha256', secret)
		.update(`${parts.t}.${rawBody}`)
		.digest('hex');
	return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}
```

Odpověz `2xx` do pár sekund. Při selhání BezBrany doručení opakuje s exponenciálním
rozestupem (1 min → 5 min → 30 min → 2 h). Endpoint musí být idempotentní — stejný
webhook může dorazit vícekrát.

## Doporučený postup implementace (checklist pro agenta)

1. Zjisti, jestli má projekt backend → zvol REST API (backend) vs. widget s `pk_` klíčem.
2. Ulož API klíč do env proměnné (např. `BEZBRANY_API_KEY`), nikdy do kódu ani do gitu.
3. Implementuj vytvoření platby při dokončení objednávky; `orderNumber` = číslo objednávky.
4. Zobraz platbu: widget (doporučeno) / vlastní QR z `qrCodeData` / redirect na hosted checkout.
5. Implementuj webhook endpoint: ověření podpisu nad raw body → idempotentní zpracování
   → označ objednávku jako zaplacenou při `status === "completed"` → vrať `200`.
6. Otestuj celý flow s `sk_test_` klíčem a `simulate-success`; pak přepni na `sk_live_`.

## Časté chyby

- ❌ Částky v haléřích (`10000` místo `100`) — API pracuje **v korunách** (decimal).
- ❌ `sk_` klíč ve frontendu — do prohlížeče patří jen `pk_`.
- ❌ Widget na doméně, která není v CORS doménách obchodníka → requesty selžou.
- ❌ Ověřování webhook podpisu nad `JSON.stringify(req.body)` místo raw body.
- ❌ Potvrzení objednávky jen podle `returnUrl` — návrat zákazníka nic negarantuje,
  jediný spolehlivý signál je webhook (nebo dotaz na stav platby přes API).
- ❌ Chybějící `Idempotency-Key` u retry logiky → duplicitní platby.
- ❌ `notificationUrl`/`returnUrl` na `http`, `localhost` nebo interní IP → v produkci
  odmítnuto (musí být https na veřejnou doménu).
- ❌ Webhook zpracovaný vícekrát bez deduplikace → použij pole `id` z payloadu.
