Přejít na hlavní obsah
Fintoro API podporuje odchádzajúce webhooky pre integrácie riadené udalosťami. Ak potrebujete na zmeny reagovať priebežne a držať externý systém zosynchronizovaný s čo najmenším oneskorením, webhooky sú správny mechanizmus. Fintoro Vám odošle HTTP POST request vždy, keď nastane vybraná obchodná udalosť. Webhooky v Fintoro API sú navrhnuté ako tenký trigger, nie ako paralelný snapshot API. Payload doručenia vždy obsahuje len identitu udalosti, jej typ a odkaz na resource cez resource.type + resource.id. Detail si následne dotiahnete z Fintoro API vo verzii, ktorú používa Vaša integrácia.

Kedy webhooky použiť

  • keď potrebujete reagovať na vytvorenie, úpravu alebo zmazanie bez toho, aby ste stav zmien stavali na periodickom dotazovaní,
  • keď si chcete lokálnu cache alebo downstream systém synchronizovať len pri reálnej zmene,
  • keď potrebujete oddeliť prijatie udalosti od následného načítania detailu resource-u.
Odporúčaný model je: webhook prijmete, overíte podpis, uložíte webhook-id na deduplikáciu a až potom si načítate detail resource-u z Fintoro API. Periodické dotazovanie nechajte len ako backfill alebo kontrolný fallback, nie ako primárny trigger zmien.

Správa odberov

Odbery webhookov spravujete priamo cez Fintoro API. Secret v otvorenej podobe sa zobrazí iba pri vytvorení odberu alebo pri jeho manuálnej rotácii. Presný CRUD kontrakt, request schémy a response payloady nájdete priamo v Webhook API referencii. Prakticky to znamená:
  • hodnotu plainTextSecret si musíte bezpečne uložiť pri create alebo rotate odpovedi,
  • zmena url ani subscribedEvents secret automaticky nerotuje,
  • isActive = false historické záznamy nevypína, len zastaví nové pokusy o doručenie.

Požiadavky na endpoint

Pri ukladaní alebo update odberu nestačí, aby url bola len syntakticky validná HTTPS adresa. Fintoro endpoint overuje prísnejšie:
  • url musí používať https://,
  • url nesmie obsahovať používateľské meno ani heslo,
  • hostname sa musí resolvnuť na verejnú IP adresu,
  • localhost, .local, privátne rozsahy, loopback, link-local a iné rezervované adresy backend odmietne.
Tá istá kontrola prebehne znovu aj tesne pred samotným odoslaním webhooku. Ak endpoint medzičasom prestane spĺňať tieto podmienky, Fintoro request vôbec neodošle a delivery označí ako neúspešné.

Payload doručenia

Payload webhooku je zámerne tenký:
{
  "id": "7f4d1861-9f6a-4fa8-a8a7-f335d7a8d515",
  "type": "invoices.updated",
  "occurredAt": "2026-03-18T15:04:05Z",
  "resource": {
    "type": "invoice",
    "id": 301
  }
}

Význam polí

PoleTypVýznam
idstringUnikátne ID webhook udalosti. Použite ho na deduplikáciu.
typestringNázov obchodnej udalosti, napríklad clients.created alebo orders.deleted.
occurredAtstringISO 8601 timestamp v UTC, kedy udalosť vznikla vo Fintoro.
resource.typestringTyp resource-u, napríklad invoice, client alebo warehouseOutboundReceipt.
resource.idintegerID resource-u vo Fintoro. Detail si podľa neho načítate z Fintoro API.
Payload neobsahuje:
  • verziu API,
  • absolútnu ani relatívnu URL na resource,
  • plný snapshot resource-u,
  • vnorené lookupy alebo vnorené obchodné objekty.
Takýto kontrakt ostáva stabilný aj vtedy, keď Vaša integrácia používa konkrétnu verziu detailových endpointov alebo vlastnú stratégiu načítania.

Delivery headery

Každý webhook request obsahuje tieto hlavičky:
HeaderVýznam
webhook-idRovnaká hodnota ako pole id v payloade.
webhook-timestampUnix timestamp v sekundách v čase odoslania.
webhook-signatureHMAC SHA-256 podpis raw JSON body pomocou secretu odberu.
webhook-id a webhook-timestamp nepodpisujete samostatne do vlastného kanonického reťazca. Podpis sa počíta nad raw request body presne v tvare, v akom prišiel.

Overenie podpisu

Overenie requestu by malo mať štyri vrstvy:
  1. skontrolujte, že request obsahuje všetky webhook headery,
  2. skontrolujte, že webhook-timestamp nie je príliš starý alebo príliš ďaleko v budúcnosti,
  3. vypočítajte HMAC SHA-256 nad raw request body a porovnajte ho v constant-time režime,
  4. deduplikujte podľa webhook-id, aby ste vedeli bezpečne spracovať opakované doručenie.

Odporúčaný flow

Pseudokód

rawBody = read_raw_request_body()
webhookId = header("webhook-id")
timestamp = integer(header("webhook-timestamp"))
providedSignature = header("webhook-signature")

if webhookId is missing or timestamp is missing or providedSignature is missing:
    return 400

if abs(current_unix_time() - timestamp) > 300:
    return 401

expectedSignature = hmac_sha256(rawBody, webhookSecret)

if not constant_time_equals(expectedSignature, providedSignature):
    return 401

if webhook_id_exists_in_dedup_store(webhookId):
    return 204

store_webhook_id(webhookId)
enqueue_fetch_job(parse_json(rawBody))

return 204

JavaScript / Node.js-like príklad

import crypto from 'node:crypto';

export async function handleFintoroWebhook(req, res) {
  // `req.rawBody` musí obsahovať pôvodný raw request body pred JSON parsingom.
  const rawBody = req.rawBody.toString('utf8');
  const webhookId = req.get('webhook-id');
  const timestamp = Number(req.get('webhook-timestamp'));
  const providedSignature = req.get('webhook-signature');

  if (!webhookId || !timestamp || !providedSignature) {
    return res.status(400).json({ error: 'Missing webhook headers.' });
  }

  if (!isFreshWebhookTimestamp(timestamp)) {
    return res.sendStatus(401);
  }

  if (!isValidWebhookSignature(rawBody, providedSignature, process.env.FINTORO_WEBHOOK_SECRET)) {
    return res.sendStatus(401);
  }

  if (await hasProcessedWebhook(webhookId)) {
    return res.sendStatus(204);
  }

  const event = JSON.parse(rawBody);

  await storeWebhookId(webhookId);
  await enqueueFetchWebhookResourceDetail({
    webhookId,
    eventType: event.type,
    resourceType: event.resource.type,
    resourceId: event.resource.id,
    companyId: event.company.id,
  });

  return res.sendStatus(204);
}

function isFreshWebhookTimestamp(timestamp) {
  return Math.abs(Math.floor(Date.now() / 1000) - timestamp) <= 300;
}

function isValidWebhookSignature(rawBody, providedSignature, secret) {
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');

  if (expectedSignature.length !== providedSignature.length) {
    return false;
  }

  return crypto.timingSafeEqual(
    Buffer.from(expectedSignature, 'utf8'),
    Buffer.from(providedSignature, 'utf8'),
  );
}

Ako odpovedať na delivery

  • Vráťte 2xx až keď ste request bezpečne prijali, overili a uložili na ďalšie spracovanie.
  • Ak signature alebo timestamp nesedia, vráťte 401 alebo inú vlastnú autentifikačnú chybu podľa politiky prijímača webhookov.
  • Pri dočasnom downstream probléme vráťte non-2xx, ak chcete, aby systém webhook zopakoval.
  • Ak request prijmete, ale ešte len zaraďujete interné spracovanie do fronty, 204 No Content je úplne v poriadku.
Fintoro používa opakované doručovanie pri neúspešnom doručení. Preto prijímač webhookov musí byť:
  • idempotentný,
  • odolný voči duplicitám,
  • schopný vrátiť 2xx aj pri znovu doručenom už spracovanom webhook-id.

Mechanizmus opakovania

Fintoro považuje doručenie za neúspešné, keď:
  • prijímač webhookov vráti non-2xx HTTP status,
  • request timeoutne,
  • nastane sieťová chyba pri odoslaní.
  • endpoint pri predodovacej kontrole už neprejde bezpečnostnou validáciou URL, DNS a IP adresy.
Aktuálna politika doručovania je:
ParameterHodnotaPoznámka
HTTP timeout10 sekúndTýka sa jedného delivery requestu
Maximálny počet pokusov51 pôvodný pokus + 4 retry pokusy
Backoff stratégiaexponenciálnaPo zlyhaniach čaká približne 10 s, 100 s, 1000 s a 10000 s
Prakticky to znamená tento typický priebeh:
PokusKedyPoznámka
1okamžiteprvé odoslanie po vzniku udalosti
2približne po 10 sretry po prvom neúspechu
3približne po 100 s od druhého pokusudruhý retry
4približne po 1000 s od tretieho pokusutretí retry
5približne po 10000 s od štvrtého pokusuposledný retry
Po vyčerpaní všetkých pokusov sa doručenie označí ako finálne neúspešné. Odber sa automaticky nevypína len preto, že jeden alebo viac pokusov zlyhalo. Z pohľadu prijímača webhookov je dôležité:
  • nepredpokladať exactly-once delivery,
  • rátať s tým, že ten istý webhook-id môže prísť opakovane,
  • odpovedať rýchlo a ťažšiu logiku delegovať do internej fronty,
  • vracať non-2xx len vtedy, keď naozaj chcete, aby Fintoro doručenie zopakovalo.

Thin payload a následné načítanie detailu

Odporúčaný integračný flow:
  1. prijmite webhook a overte podpis,
  2. deduplikujte podľa webhook-id,
  3. podľa type a resource určite, ktorý endpoint detailu potrebujete,
  4. detail si načítajte z Fintoro API vo verzii, ktorú používa Vaša integrácia,
  5. business logiku robte až nad načítaným detailom.
Príklady:
  • clients.updated + resource.id = 42GET /clients/42
  • invoices.created + resource.id = 301GET /invoices/301
  • warehouse-outbound-receipts.deleted + resource.id = 88 → ak už detail neexistuje, spracujte delete lokálne len podľa payloadu eventu
Pri delete eventoch rátajte s tým, že detail resource-u už nemusí byť dostupný. V takom prípade je zdrojom pravdy samotný webhook event a Vaše lokálne mapovanie.

Dostupné eventy

Master data

EventResourcePopis
clients.createdclientNový klient bol vytvorený.
clients.updatedclientExistujúci klient bol zmenený.
clients.deletedclientKlient bol zmazaný.
suppliers.createdsupplierNový dodávateľ bol vytvorený.
suppliers.updatedsupplierExistujúci dodávateľ bol zmenený.
suppliers.deletedsupplierDodávateľ bol zmazaný.
bank-accounts.createdbankAccountBankový účet bol vytvorený.
bank-accounts.updatedbankAccountBankový účet bol zmenený.
bank-accounts.deletedbankAccountBankový účet bol zmazaný.

CRM

EventResourcePopis
business-case-statuses.createdbusinessCaseStatusBol vytvorený nový stav obchodného prípadu.
business-case-statuses.updatedbusinessCaseStatusStav obchodného prípadu bol zmenený.
business-case-statuses.deletedbusinessCaseStatusStav obchodného prípadu bol zmazaný.
business-cases.createdbusinessCaseBol vytvorený nový obchodný prípad.
business-cases.updatedbusinessCaseObchodný prípad bol zmenený.
business-cases.deletedbusinessCaseObchodný prípad bol zmazaný.
contact-activity-logs.createdcontactActivityLogBola vytvorená CRM udalosť.
contact-activity-logs.updatedcontactActivityLogCRM udalosť bola zmenená.
contact-activity-logs.deletedcontactActivityLogCRM udalosť bola zmazaná.

Sklady a katalóg

EventResourcePopis
warehouses.createdwarehouseBol vytvorený nový sklad.
warehouses.updatedwarehouseSklad bol zmenený.
warehouses.deletedwarehouseSklad bol zmazaný.
warehouse-inbound-receipts.createdwarehouseInboundReceiptBola vytvorená skladová príjemka.
warehouse-inbound-receipts.updatedwarehouseInboundReceiptSkladová príjemka bola zmenená.
warehouse-inbound-receipts.deletedwarehouseInboundReceiptSkladová príjemka bola zmazaná.
warehouse-outbound-receipts.createdwarehouseOutboundReceiptBola vytvorená skladová výdajka.
warehouse-outbound-receipts.updatedwarehouseOutboundReceiptSkladová výdajka bola zmenená.
warehouse-outbound-receipts.deletedwarehouseOutboundReceiptSkladová výdajka bola zmazaná.
price-list-items.createdpriceListItemBola vytvorená nová skladová alebo cenníková položka.
price-list-items.updatedpriceListItemSkladová alebo cenníková položka bola zmenená.
price-list-items.deletedpriceListItemSkladová alebo cenníková položka bola zmazaná.
price-list-items.stock-updatedpriceListItemZmenil sa skladový stav položky cenníka, napríklad po vytvorení, úprave alebo zmazaní skladovej príjemky či výdajky.

Doklady

EventResourcePopis
invoices.createdinvoiceBola vytvorená faktúra.
invoices.updatedinvoiceFaktúra bola zmenená.
invoices.deletedinvoiceFaktúra bola zmazaná.
invoices.paidinvoiceFaktúra sa po zaevidovaní úhrady stala plne uhradenou.
credit-notes.createdcreditNoteBol vytvorený dobropis.
credit-notes.updatedcreditNoteDobropis bol zmenený.
credit-notes.deletedcreditNoteDobropis bol zmazaný.
credit-notes.paidcreditNoteDobropis sa po zaevidovaní úhrady stal plne uhradeným.
received-invoices.paidreceivedInvoicePrijatá faktúra sa po zaevidovaní úhrady stala plne uhradenou.
received-receipts.paidreceivedReceiptPrijatý bloček sa po zaevidovaní úhrady stal plne uhradeným.
proformas.createdproformaBola vytvorená zálohová faktúra.
proformas.updatedproformaZálohová faktúra bola zmenená.
proformas.deletedproformaZálohová faktúra bola zmazaná.
proformas.paidproformaZálohová faktúra sa po zaevidovaní úhrady stala plne uhradenou.
orders.createdorderBola vytvorená objednávka.
orders.updatedorderObjednávka bola zmenená.
orders.deletedorderObjednávka bola zmazaná.
quotations.createdquotationBola vytvorená cenová ponuka.
quotations.updatedquotationCenová ponuka bola zmenená.
quotations.deletedquotationCenová ponuka bola zmazaná.
document-payments.createddocumentPaymentBola zaevidovaná úhrada dokladu.
document-payments.deleteddocumentPaymentÚhrada dokladu bola zmazaná.

Praktické odporúčania pre produkciu

  • nepúšťajte ťažkú business logiku priamo vo vlákne HTTP requestu prijímača webhookov,
  • raw payload aj webhook-id si ukladajte pred ďalším spracovaním,
  • secret rotujte kontrolovane a s plánom výmeny na strane prijímača webhookov,
  • sledujte retry vzory a opakované non-2xx odpovede ako indikátor incidentu na strane prijímača webhookov,
  • logujte si webhook-id, type, resource.type, resource.id a X-Request-Id z následného načítania detailu,
  • pri delete eventoch sa nespoliehajte na to, že detail endpoint ešte stále vráti resource.

Súvisiace sekcie