KYB pre e-shopy v troch volaniach
Zákazník zadá IČO do B2B registračného formulára. Kým klikne na odoslať, viete jeho obchodné meno a adresu, či je jeho IČ DPH platné vo VIES, a či mu máte fakturovať na faktúru, žiadať platbu vopred alebo účet odmietnuť.
Všetky štyri volania bežia na vašom serveri
Origin pinning — čo sa reálne vynucuje
Origin pinning tu existuje, ale pripína embed tokeny, nie API kľúče. Sú to dva odlišné mechanizmy a záleží na tom, o ktorý sa opierate:
| Mechanizmus | Pripnuté na | Čo chráni |
|---|---|---|
sk_ API kľúč | Nič. Kľúč nemá stĺpec pre origin, doménu ani referrer. | Iba utajenie. Držte ho na serveri. |
| CORS allow-list | Pevný zoznam Entyrix / NISMap hostov, rovnaký pre každý kľúč. | Vaša doména na ňom nie je a nedá sa pridať per kľúč — fetch z prehliadača z vášho originu nedostane Access-Control-Allow-Origin a prehliadač odpoveď zahodí. Platí to aj pre neautentifikované /public/* endpointy. |
/widget-token | Jeden presný host, podpísaný v HMAC claime. | Jediný schválený povrch priamo pre prehliadač. Overuje sa proti Origin, potom Referer; požiadavka bez oboch je odmietnutá. |
Praktický dôsledok: ani našeptávač, ani detail firmy, ani credit score, ani monitoring nie sú dosiahnuteľné z prehliadača zákazníka. Váš PHP alebo Node backend volá Entyrix; prehliadač volá iba váš vlastný origin. S touto architektúrou počíta recept nižšie.
Ak predsa len chcete niečo v prehliadači
Vytvorte si na serveri embed token pripnutý na doménu a do stránky pošlite iba ten. Pokrýva widgety credit, financials a graph — nie našeptávač, nie detail firmy, nie monitoring. Token je dlhoveký (30 dní default, 90 maximum) a zhoda hosta je presná: token viazaný na shop.example je pre www.shop.example odmietnutý.
# Server-side: mint a token pinned to your shop's host.
curl -X POST -H "Authorization: Bearer $ENTYRIX_API_KEY" \
-H "Content-Type: application/json" \
-d '{"domain":"shop.example","widget_types":["credit"],"ttl_seconds":2592000}' \
"https://entyrix.com/api/v1/widget-token"
# Then embed the returned token in your page:
# <script src="https://entyrix.com/api/v1/widget.js"
# data-widget="credit" data-ico="31322832" data-country="SK"
# data-token="..."></script>
Našeptávač IČO
Formulár pošle napísaný reťazec na váš vlastný endpoint; váš backend ho proxuje sem. Vyžadujte aspoň dva znaky — kratšie dopyty sú šum.
curl -H "Authorization: Bearer $ENTYRIX_API_KEY" \ "https://entyrix.com/api/v1/companies/autocomplete?q=slovnaft&limit=3"
{
"data": [
{
"ico": "31322832",
"name": "SLOVNAFT, a.s.",
"legalFormName": "Akciová spoločnosť",
"municipality": "Bratislava",
"address": "Vlčie hrdlo 1",
"status": "active",
"country": "SK",
"subjectType": "legal_entity"
}
],
"meta": { "request_id": "req_b4b79236af1528c5", "duration_ms": 66,
"estimatedTotalHits": 65, "cached": false }
}
V dropdowne zobrazte meno, IČO a obec — obec je to, čo odlíši takmer identické názvy v rámci skupiny. Pole status hneď povie, či je subjekt aktívny, v likvidácii alebo zaniknutý; zošednutie zaniknutej zhody vám neskôr ušetrí support ticket.
Autofill adresy, potvrdenie IČ DPH
Dve volania: detail firmy vyplní fakturačné polia, compliance súhrn povie, či je IČ DPH platné vo VIES a či na subjekte niečo diskvalifikujúce nevisí.
curl -H "Authorization: Bearer $ENTYRIX_API_KEY" \ "https://entyrix.com/api/v1/companies/31322832" curl -H "Authorization: Bearer $ENTYRIX_API_KEY" \ "https://entyrix.com/api/v1/companies/31322832/compliance"
// GET /companies/31322832 — trimmed to the fields a checkout needs
{
"data": {
"ico": "31322832",
"name": "SLOVNAFT, a.s.",
"street": "Vlčie hrdlo",
"buildingNumber": "1",
"postalCode": "82412",
"municipality": "Bratislava",
"country": "SK",
"dic": "2020372640",
"icDph": "SK7120001713",
"vatId": "SK7120001713",
"status": "active",
"isActive": true,
"creditScore": 100,
"creditGrade": "A+"
},
"meta": { "request_id": "req_77ac8f6bcb216b0d", "duration_ms": 28, "cached": false }
}
// GET /companies/31322832/compliance — trimmed
{
"data": {
"overall": "STANDARD",
"recommendation": "Standard due diligence postačuje",
"flagsCount": { "critical": 0, "high": 0, "medium": 0, "low": 0 },
"signals": {
"sanctions": { "isSanctioned": false, "isDebarred": false },
"insolvency": { "inBankruptcy": false, "inLiquidation": false, "inRestructuring": false },
"tax": {
"reliability": "vysoko spoľahlivý",
"hasTaxDebt": false,
"isVatPayer": true,
"viesValid": true,
"viesValidatedAt": "2026-08-18T02:33:09.326Z"
}
}
},
"meta": { "request_id": "req_49206dde71d73f9b", "duration_ms": 19, "cached": false }
}
Čítajte vatId, nie vlastný COALESCE z icDph a dic — vatId je kanonické číslo v tvare pre VIES a je už odvodené za vás. dic ostáva holý daňový identifikátor, icDph ostáva národný tvar s prefixom. viesValidatedAt povie, aká čerstvá je odpoveď z VIES.
Kreditná brána
Jedno volanie rozhodne o platobných podmienkach. Vetvite najprv na hardStop a až potom na score — hardStop je kategorická diskvalifikácia, nie nízke číslo.
curl -H "Authorization: Bearer $ENTYRIX_API_KEY" \ "https://entyrix.com/api/v1/companies/36620319/credit-score"
// A healthy counterparty — hardStop is null, invoice on terms
{
"data": {
"ico": "31322832", "companyName": "SLOVNAFT, a.s.",
"score": 100, "grade": "A+", "baseline": 70,
"hardStop": null,
"warnings": []
}
}
// A company in bankruptcy — hardStop is set, block regardless of score
{
"data": {
"ico": "36620319", "companyName": "Potraviny Kačka, a.s. „v konkurze“",
"score": 0, "grade": "F", "baseline": 70,
"hardStop": "bankruptcy",
"factors": [
{ "name": "bankruptcy", "delta": -80, "note": "Konkurzné konanie" },
{ "name": "socialInsuranceDebt", "delta": -15, "note": "SocPoist dlh €35,283" },
{ "name": "negativeEquity", "delta": -25, "note": "Záporné vlastné imanie €17,451,133" }
],
"warnings": ["KRITICKÉ: firma v konkurze", "Záporné vlastné imanie (technický bankrot)"]
}
}
Rozumné default mapovanie: hardStop nastavený znamená odmietnuť účet alebo iba platbu vopred; score pod 40 znamená platbu vopred; nad tým fakturujte v štandardnom režime. Prah si dolaďte podľa vlastnej tolerancie nedobytných pohľadávok — pole factors ukazuje presne to, čo číslom pohlo, takže odmietnutie viete zákazníkovi vysvetliť.
Sledovanie aj po objednávke
Kreditná previerka je momentka. Prihláste sa raz pri registrácii a o konkurze zákazníka sa dozviete aj o mesiace neskôr.
curl -X POST -H "Authorization: Bearer $ENTYRIX_API_KEY" \
-H "Content-Type: application/json" \
-d '{"ico":"31322832","webhook_url":"https://shop.example/entyrix","tier":"free"}' \
"https://entyrix.com/api/v1/monitoring/subscriptions"
// 201 Created — webhookSecret is shown ONCE. Store it now.
{
"data": {
"id": 4,
"ico": "31322832",
"webhookUrl": "https://shop.example/entyrix",
"webhookSecret": "84d679a7…963de4d",
"webhookVersion": 2,
"digestFrequency": "immediate",
"tier": "free",
"email": null,
"createdAt": "2026-08-30T16:11:01.440Z"
},
"meta": {
"request_id": "req_b4f42e5e7a281251",
"notice": "webhookSecret sa zobrazuje IBA raz pri vytvorení. …"
}
}
Subscription je per firma a doručuje všetky typy udalostí, ktoré tier dovolí — neexistuje filter na podmnožinu typov ani pole events[] či company_filter v tele požiadavky. Free tier doručuje 8 z 33 typov.
Pokrytie je medzi trhmi naozaj nerovnomerné — päť typov fíruje všade, kde držíme záznam z registra, viacero je len SK/CZ a dva nemajú v produkcii dáta vôbec. Nesľubujte obchodníkom signál, ktorý v ich krajine nefíruje: tabuľka zdroja, závažnosti, latencie a pokrytia per typ je v docs/api-reference.md § Monitoring a ten istý zoznam je strojovo čitateľný v OpenAPI spec pod MonitoringEventType.
Čo príde na váš endpoint
POST /entyrix HTTP/1.1 Content-Type: application/json User-Agent: Entyrix-Webhook/1.0 X-Entyrix-Event-ID: 3f1c… ← idempotency key, dedupe on this X-Entyrix-Timestamp: 1788106367000 ← decimal ms since epoch X-Entyrix-Webhook-Version: 2 X-Entyrix-Signature: sha256=<hex> ← HMAC over "<timestamp>.<raw body>"
Podpis pokrýva timestamp a surové telo spojené bodkou, takže telo musíte prečítať skôr, než ho JSON middleware pretransformuje. Odmietnite čokoľvek staršie než 5 minút, porovnávajte v konštantnom čase a deduplikujte podľa X-Entyrix-Event-ID: odpoveď mimo 2xx sa štyrikrát opakuje s exponenciálnym backoffom, takže tá istá udalosť môže legitímne prísť viackrát.
<?php
/**
* Entyrix webhook receiver (signature v2).
* Read the RAW body — a parsed/re-encoded body will not match the signature.
*/
$rawBody = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_ENTYRIX_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_ENTYRIX_SIGNATURE'] ?? '';
$secret = getenv('ENTYRIX_WEBHOOK_SECRET'); // shown ONCE, at subscribe time
function entyrix_verify_webhook(
string $rawBody,
string $timestamp,
string $signature,
string $secret
): bool {
if (!preg_match('/^\d+$/', $timestamp)) {
return false;
}
$nowMs = (int) round(microtime(true) * 1000);
if (abs($nowMs - (int) $timestamp) > 300000) {
return false; // outside the 5-minute replay window
}
$expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);
return hash_equals($expected, $signature); // constant-time
}
if (!entyrix_verify_webhook($rawBody, $timestamp, $signature, $secret)) {
http_response_code(401);
exit;
}
$event = json_decode($rawBody, true);
// Deduplicate on $event['eventId'] — a retry re-sends the same id.
// $event['eventType'] is the stable code; never branch on the Slovak summary.
if ($event['eventType'] === 'bankruptcy_change') {
// e.g. flip the customer to prepay-only
}
http_response_code(200); // anything outside 2xx is retriedimport { createHmac, timingSafeEqual } from "node:crypto";
/** Verify an Entyrix webhook (signature v2). */
export function verifyWebhook(rawBody, timestamp, signature, secret) {
if (!/^\d+$/.test(timestamp)) return false;
if (Math.abs(Date.now() - Number(timestamp)) > 300000) return false; // 5-min replay window
const expected =
"sha256=" + createHmac("sha256", secret).update(timestamp + "." + rawBody).digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(signature);
return a.length === b.length && timingSafeEqual(a, b); // constant-time
}
// Express — mount with a RAW body parser, not express.json():
// app.post("/entyrix", express.raw({ type: "application/json" }), handler)
export function handler(req, res) {
const rawBody = req.body.toString("utf8");
const ok = verifyWebhook(
rawBody,
req.get("X-Entyrix-Timestamp") ?? "",
req.get("X-Entyrix-Signature") ?? "",
process.env.ENTYRIX_WEBHOOK_SECRET
);
if (!ok) return res.sendStatus(401);
const event = JSON.parse(rawBody);
// Deduplicate on event.eventId — a retry re-sends the same id.
// event.eventType is the stable code; never branch on the Slovak summary.
if (event.eventType === "bankruptcy_change") {
// e.g. flip the customer to prepay-only
}
res.sendStatus(200); // anything outside 2xx is retried
}Oba súbory sme pred zverejnením spustili proti produkcii a vrátili identický výsledok.
Chyby a rate limity
Každá chyba nesie meta.request_id — uveďte ho, keď sa nás pýtate na konkrétne volanie.
| Status | Význam | Čo má kód spraviť |
|---|---|---|
| 400 | INVALID_COUNTRY, INVALID_ICO — chybný vstup | Opravte požiadavku; neopakujte. |
| 401 | UNAUTHORIZED — chýbajúci alebo neznámy kľúč | Upozornite operátora. Nikdy neopakujte v slučke. |
| 403 | FORBIDDEN — kľúč vypnutý alebo expirovaný | Upozornite operátora. |
| 404 | Subjekt nenájdený alebo za FO bránou. Kódy sa medzi routami líšia (NOT_FOUND, COMPANY_NOT_FOUND, FO_GATED_NO_DPA). | Vetvite na HTTP status, nie na error.code. Prepnite na ručné zadanie. |
| 429 | RATE_LIMITED — pozri dva limity nižšie | Počkajte Retry-After sekúnd a skúste raz znova. Nebúšte. |
| 5xx | Prechodné zlyhanie. | Zopakujte raz s backoffom, potom nechajte prejsť na ručnú kontrolu. |
Dva limity, nie jeden
Zdieľaný strop zhruba 600 požiadaviek za minútu sa počíta podľa vášho bearer tokenu (pri neautentifikovaných volaniach podľa IP) a samostatná per-kľúč kvóta je štandardne 120 za minútu v pevnom 60-sekundovom okne. Oba vracajú X-RateLimit-Limit, X-RateLimit-Remaining a X-RateLimit-Reset; Retry-After pridáva iba zdieľaný limiter, a preto snippety pri chýbajúcej hlavičke čakajú jednu sekundu.
Registračný formulár sa ani k jednému limitu nepriblíži. Hromadné prepočítanie existujúcej zákazníckej bázy áno — stránkujte ho, alebo použite bulk endpoint, ktorý berie 100 identifikátorov naraz.
Copy-paste klient
Všetky tri kroky vrátane retry a ošetrenia chýb. ENTYRIX_API_KEY nastavte v prostredí servera — nie v súbore šablóny, nie v JS bundli.
<?php
/**
* Entyrix KYB — server-side B2B registration check.
* Drop into modules/yourmodule/src/EntyrixKyb.php (PrestaShop) or any PSR-4 tree.
* Runs on your server ONLY. The sk_ key must never reach the browser.
*/
final class EntyrixKyb
{
private const BASE = 'https://entyrix.com/api/v1';
public function __construct(private string $apiKey) {}
/** Low-level call. Retries once on 429/5xx, honouring Retry-After. */
private function call(string $path, array $query = []): array
{
$url = self::BASE . $path . ($query ? '?' . http_build_query($query) : '');
for ($attempt = 0; $attempt < 2; $attempt++) {
$retryAfter = 0;
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 3,
CURLOPT_TIMEOUT => 8,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $this->apiKey,
'Accept: application/json',
],
CURLOPT_HEADERFUNCTION => function ($ch, $header) use (&$retryAfter) {
if (stripos($header, 'retry-after:') === 0) {
$retryAfter = (int) trim(substr($header, 12));
}
return strlen($header);
},
]);
$raw = curl_exec($ch);
$status = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$err = curl_error($ch);
curl_close($ch);
if ($raw === false) {
return ['status' => 0, 'body' => null, 'error' => $err];
}
if (($status === 429 || $status >= 500) && $attempt === 0) {
sleep(max(1, min($retryAfter, 5)));
continue;
}
return ['status' => $status, 'body' => json_decode($raw, true), 'error' => null];
}
return ['status' => 0, 'body' => null, 'error' => 'unreachable'];
}
/** Step 1 — IČO autocomplete for the registration form. */
public function suggest(string $q, int $limit = 8): array
{
if (mb_strlen(trim($q)) < 2) {
return [];
}
$r = $this->call('/companies/autocomplete', ['q' => $q, 'limit' => $limit]);
return $r['status'] === 200 ? ($r['body']['data'] ?? []) : [];
}
/** Step 2 — autofill + VIES. null when absent, terminated-unknown or FO-gated. */
public function profile(string $ico): ?array
{
$detail = $this->call('/companies/' . rawurlencode($ico));
// 404 also covers sole traders: error.code = FO_GATED_NO_DPA.
if ($detail['status'] !== 200) {
return null;
}
$c = $detail['body']['data'];
$comp = $this->call('/companies/' . rawurlencode($ico) . '/compliance');
$vies = $comp['status'] === 200
? ($comp['body']['data']['signals']['tax']['viesValid'] ?? null)
: null;
return [
'name' => $c['name'],
'street' => $c['street'],
'city' => $c['municipality'],
'postcode' => $c['postalCode'],
'country' => $c['country'],
'vat_id' => $c['vatId'],
'dic' => $c['dic'],
'status' => $c['status'],
'vies_valid' => $vies,
];
}
/** Step 3 — credit gate. */
public function creditGate(string $ico): array
{
$r = $this->call('/companies/' . rawurlencode($ico) . '/credit-score');
if ($r['status'] !== 200) {
// Fail OPEN to manual review — never silently approve on an outage.
return ['decision' => 'review', 'reason' => 'score_unavailable'];
}
$d = $r['body']['data'];
if (!empty($d['hardStop'])) {
return ['decision' => 'block', 'reason' => $d['hardStop'], 'grade' => $d['grade']];
}
if ((int) $d['score'] < 40) {
return ['decision' => 'prepay', 'reason' => 'low_score', 'grade' => $d['grade']];
}
return ['decision' => 'invoice', 'grade' => $d['grade'], 'score' => $d['score']];
}
}/**
* Entyrix KYB — server-side B2B registration check (Node 18+).
* Runs on your server ONLY. The sk_ key must never reach the browser.
*/
const BASE = "https://entyrix.com/api/v1";
export class EntyrixKyb {
constructor(apiKey) {
this.apiKey = apiKey;
}
/** Low-level call. Retries once on 429/5xx, honouring Retry-After. */
async call(path, query = {}) {
const url = new URL(BASE + path);
for (const [k, v] of Object.entries(query)) url.searchParams.set(k, String(v));
for (let attempt = 0; attempt < 2; attempt++) {
let res;
try {
res = await fetch(url, {
headers: { Authorization: "Bearer " + this.apiKey, Accept: "application/json" },
signal: AbortSignal.timeout(8000),
});
} catch (err) {
return { status: 0, body: null, error: String(err) };
}
if ((res.status === 429 || res.status >= 500) && attempt === 0) {
const wait = Number(res.headers.get("retry-after") || 1);
await new Promise((r) => setTimeout(r, Math.min(Math.max(wait, 1), 5) * 1000));
continue;
}
return { status: res.status, body: await res.json().catch(() => null), error: null };
}
return { status: 0, body: null, error: "unreachable" };
}
/** Step 1 — IČO autocomplete for the registration form. */
async suggest(q, limit = 8) {
if (q.trim().length < 2) return [];
const r = await this.call("/companies/autocomplete", { q, limit });
return r.status === 200 ? (r.body?.data ?? []) : [];
}
/** Step 2 — autofill + VIES. null when absent or FO-gated. */
async profile(ico) {
const detail = await this.call("/companies/" + encodeURIComponent(ico));
// 404 also covers sole traders: error.code = FO_GATED_NO_DPA.
if (detail.status !== 200) return null;
const c = detail.body.data;
const comp = await this.call("/companies/" + encodeURIComponent(ico) + "/compliance");
const viesValid =
comp.status === 200 ? (comp.body?.data?.signals?.tax?.viesValid ?? null) : null;
return {
name: c.name,
street: c.street,
city: c.municipality,
postcode: c.postalCode,
country: c.country,
vatId: c.vatId,
dic: c.dic,
status: c.status,
viesValid,
};
}
/** Step 3 — credit gate. */
async creditGate(ico) {
const r = await this.call("/companies/" + encodeURIComponent(ico) + "/credit-score");
// Fail OPEN to manual review — never silently approve on an outage.
if (r.status !== 200) return { decision: "review", reason: "score_unavailable" };
const d = r.body.data;
if (d.hardStop) return { decision: "block", reason: d.hardStop, grade: d.grade };
if (d.score < 40) return { decision: "prepay", reason: "low_score", grade: d.grade };
return { decision: "invoice", grade: d.grade, score: d.score };
}
}Oba súbory sme pred zverejnením spustili proti produkcii a vrátili identický výsledok.
Zapojenie do PrestaShopu
profile() volajte z AJAX controllera za vlastným front-controller tokenom a výsledok zapíšte do polí adresy; creditGate() volajte z actionValidateCustomerAddressForm alebo z vlastného registračného hooku. Kľúč držte v $_SERVER alebo v parameters súbore — kľúč v config/settings.inc.php commitnutý do repozitára shopu je najčastejší spôsob, ako uniknú.
Potrebujete kľúč?
Napíšte na [email protected] a povedzte, do ktorých trhov predávate — pokrytie sa líši podľa krajiny a férovo vám povieme, čo kde funguje.