Rýchly štart

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ť.

Skôr než začnete

Všetky štyri volania bežia na vašom serveri

Neexistuje API kľúč bezpečný pre prehliadač. Každý kľúč Entyrix je tajný kľúč sk_ s plnými právami účtu, verejná varianta pk_ neexistuje. Kľúč poslaný do prehliadača vystaví celú vašu kvótu a všetky endpointy s kľúčom každému, kto otvorí vývojárske nástroje prehliadača.

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:

MechanizmusPripnuté 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 CORSPevný 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-tokenJedna 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>
Volania zo servera nemajú hlavičku Origin. Pre API to nevadí, pre token widgetu áno. REST API pri chýbajúcom Origin predpokladá volajúceho mimo prehliadača a pokračuje. Dáta widgetu fungujú naopak: bez Origin aj Referer nevedia overiť pripnutú doménu a vrátia 403 DOMAIN_NOT_ALLOWED. Dáta widgetu preto neposielajte cez svoj server.
Krok 1

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.

Krok 2

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.

Živnostníci sú štandardne za bránou. Záznamy fyzických osôb (živnostníci) vracajú 404 s error.code = FO_GATED_NO_DPA a meta.foGated = true, kým k vášmu kľúču nie je podpísaný dodatok o spracúvaní údajov fyzických osôb. Ide o veľkú časť firemných zákazníkov e-shopov, takže si vopred určte, čo s tým formulár spraví: ponúknite ručné zadanie, nie hlášku, že IČO neexistuje.
Krok 3

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ť.

Krok 4 (nepovinný)

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 retried
import { 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.

Chybové stavy

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.

StavVýznamČo má kód spraviť
400INVALID_COUNTRY, INVALID_ICO — chybný vstupOpravte požiadavku; neopakujte.
401UNAUTHORIZED — chýbajúci alebo neznámy kľúčUpozornite operátora. Nikdy neopakujte v slučke.
403FORBIDDEN — kľúč vypnutý alebo s uplynutou platnosťouUpozornite operátora.
404Subjekt 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.
429RATE_LIMITED — pozrite si dva limity nižšiePočkajte Retry-After sekúnd a skúste to raz znova. Neopakujte požiadavky naslepo.
5xxPrechodné 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.

Recept

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.