KYB pre e-shopy v troch volaniach
Zákazník zadá IČO do registračného formulára pre firmy. 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ť so splatnosťou, žiadať platbu vopred alebo účet odmietnuť.
Všetky štyri volania bežia na vašom serveri
Pripnutie na doménu: čo sa naozaj vynucuje
Pripnutie na doménu tu existuje, ale platí pre tokeny vložených widgetov, nie pre 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á pole pre pôvod (Origin), doménu ani Referer. | Iba utajenie. Držte ho na serveri. |
| Povolené domény CORS | Pevný zoznam domén Entyrix a NISMap, rovnaký pre každý kľúč. | Vaša doména na ňom nie je a nedá sa pridať ku kľúču. Požiadavka z prehliadača z vašej domény nedostane hlavičku Access-Control-Allow-Origin a prehliadač odpoveď zahodí. Platí to aj pre verejné endpointy /public/* bez kľúča. |
/widget-token | Jedna presná doména, podpísaná v tokene HMAC. | Jediný schválený spôsob priamo z prehliadača. Overuje sa proti hlavičke Origin, potom Referer; požiadavka bez oboch je odmietnutá. |
V praxi to znamená, že ani našeptávač, ani detail firmy, ani kreditné skóre, ani sledovanie zmien nie sú dosiahnuteľné z prehliadača zákazníka. Entyrix volá váš server v PHP alebo Node; prehliadač volá iba vašu vlastnú doménu. S takýmto usporiadaním počíta recept nižšie.
Ak predsa len chcete niečo v prehliadači
Vytvorte si na serveri token widgetu pripnutý na doménu a do stránky pošlite iba ten. Pokrýva widgety bonity, financií a prepojení, nie našeptávač, detail firmy ani sledovanie zmien. Token platí dlho (predvolene 30 dní, najviac 90) a doména sa musí zhodovať presne: 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ý text na váš vlastný endpoint a váš server ho prepošle sem. Vyžadujte aspoň dva znaky, kratšie dopyty nemajú zmysel.
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 ponuke zobrazte meno, IČO a obec, lebo práve obec odlíši takmer rovnaké názvy firiem v jednej skupine. Pole status hneď povie, či je subjekt aktívny, v likvidácii alebo zaniknutý; ak zaniknutú firmu zobrazíte sivou, ušetríte si neskôr otázky zákazníkov.
Doplnenie adresy, overenie IČ DPH
Dve volania: detail firmy vyplní fakturačné polia a súhrn rizík povie, či je IČ DPH platné vo VIES a či na firme nie je niečo, pre čo ju treba odmietnuť.
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é spájanie icDph a dic: vatId je číslo v tvare pre VIES a je už odvodené za vás. dic ostáva holé daňové identifikačné číslo, icDph ostáva národný tvar s predponou krajiny. viesValidatedAt povie, aká čerstvá je odpoveď z VIES.
Kreditná brána
Jedno volanie rozhodne o platobných podmienkach. Najprv sa rozhodujte podľa hardStop a až potom podľa score: hardStop znamená, že firmu treba odmietnuť bez ohľadu na čí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é východiskové nastavenie: ak je hardStop nastavený, účet odmietnite alebo pripustite len platbu vopred; score pod 40 znamená platbu vopred; nad tým fakturujte so svojou bežnou splatnosťou. Prah si upravte podľa toho, koľko nezaplatených faktúr znesiete. Pole factors ukazuje presne to, čo číslo ovplyvnilo, takže odmietnutie viete zákazníkovi vysvetliť.
Sledovanie aj po objednávke
Preverenie bonity platí k jednému dňu. Zapnite sledovanie 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í. …"
}
}
Sledovanie sa zakladá pre jednu firmu a doručuje všetky typy udalostí, ktoré váš plán dovolí. Filter na vybrané typy neexistuje, v tele požiadavky nie je pole events[] ani company_filter. Plán Free doručuje 8 z 34 typov.
Pokrytie je medzi trhmi naozaj nerovnomerné: päť typov chodí všade, kde máme záznam z registra, viaceré len pre SK a CZ a dva zatiaľ nemajú dáta vôbec. Nesľubujte obchodníkom upozornenie, ktoré v ich krajine nechodí. Zoznam typov udalostí je strojovo čitateľný v špecifikácii OpenAPI pod MonitoringEventType; pokrytie podľa krajín vám povieme na požiadanie.
Č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 časovú pečiatku a surové telo spojené bodkou, takže telo musíte prečítať skôr, než ho spracovanie JSON zmení. Odmietnite všetko staršie než 5 minút, porovnávajte v konštantnom čase a duplicity rozpoznávajte podľa X-Entyrix-Event-ID: pri odpovedi mimo 2xx doručenie opakujeme štyrikrát s narastajúcim odstupom, takže tá istá udalosť môže 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 limity požiadaviek
Každá chyba nesie meta.request_id. Uveďte ho, keď sa nás pýtate na konkrétne volanie.
| Stav | 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 s uplynutou platnosťou | Upozornite operátora. |
| 404 | Subjekt nenájdený alebo chránený ako fyzická osoba. Kódy sa medzi endpointmi líšia (NOT_FOUND, COMPANY_NOT_FOUND, FO_GATED_NO_DPA). | Rozhodujte sa podľa stavového kódu HTTP, nie podľa error.code. Prepnite na ručné zadanie. |
| 429 | RATE_LIMITED — pozrite si dva limity nižšie | Počkajte Retry-After sekúnd a skúste to raz znova. Neopakujte požiadavky naslepo. |
| 5xx | Prechodné zlyhanie. | Zopakujte raz s odstupom, potom pošlite registráciu na ručnú kontrolu. |
Dva limity, nie jeden
Dve nezávislé vrstvy. Na kľúč: limit plánu v pevnom 60-sekundovom okne (Free 120 za minútu, ďalšie plány v cenníku). Zdieľaná: pri volaní bez kľúča ~600 požiadaviek za minútu na IP, na kľúč ~600 alebo limit jeho plánu, ak je vyšší; iba táto vrstva posiela Retry-After. Obe posielajú X-RateLimit-Limit, X-RateLimit-Remaining a X-RateLimit-Reset. Preto ukážky kódu pri chýbajúcej hlavičke Retry-After čakajú jednu sekundu.
Registračný formulár sa ani k jednému limitu nepriblíži. Hromadné prepočítanie existujúcich zákazníkov áno: rozdeľte ho na časti alebo použite hromadný endpoint, ktorý berie 100 identifikátorov naraz.
Klient na skopírovanie
Všetky tri kroky vrátane opakovania a ošetrenia chýb. ENTYRIX_API_KEY nastavte v prostredí servera, nie v súbore šablóny ani v balíku JavaScriptu.
<?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 kontroléra chráneného vlastným 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 súbore s parametrami. Kľúč v config/settings.inc.php uložený v repozitári obchodu je najčastejší spôsob, ako kľúče uniknú.
Potrebujete kľúč?
Prihláste sa a kľúč Free dostanete hneď. Pokrytie sa líši podľa krajiny: napíšte na [email protected], do ktorých trhov predávate, a férovo vám povieme, čo kde funguje.