# Ověření a testování (/cs/webhooks/receiving)



Před použitím payloadu ověřte každý webhook a poté odešlete test, abyste si potvrdili, že příjemce funguje od začátku do konce.

## Než začnete [#before-you-start]

* [Uložená cílová adresa](/webhooks/configuration#save-a-destination) a její podpisový klíč `twhsec_...`. K ověřování webhooků nepoužívejte Server Secret Token.
* Routa příjemce, která dokáže načíst původní tělo požadavku dřív, než ho změní jakýkoli JSON parser.
* Úložiště s unikátním omezením na `id` události, aby byly duplicity bezpečné.

## Přijetí webhooku [#receive-a-webhook]

<div className="fd-steps [&_h3]:fd-step">
  ### Načtěte původní tělo [#read-the-raw-body]

  Tapaya posílá JSON požadavek `POST` s těmito hlavičkami. Tělo ponechte jako původní bajty; rozparsované a znovu serializované tělo už podpisu neodpovídá.

  ```http
  Content-Type: application/json
  X-Tapaya-Event-Id: 019a0000-0000-7000-8000-000000000001
  X-Tapaya-Signature: t=1793361600,v1=<lowercase hexadecimal HMAC-SHA256>
  ```

  * V Express použijte pro tuto routu `express.raw({ type: "application/json" })` před globálním JSON parserem.

  ### Ověřte podpis [#check-the-signature]

  Vypočítejte HMAC-SHA256 z časového razítka, tečky a původního těla a porovnejte ho s `v1` v konstantním čase.

  ```js title="verify-tapaya-webhook.mjs"
  import { createHmac, timingSafeEqual } from "node:crypto";

  export function verifyTapayaWebhook(rawBody, signatureHeader, secret, now = Date.now()) {
    if (!Buffer.isBuffer(rawBody) || typeof signatureHeader !== "string") {
      throw new Error("Missing raw body or signature");
    }
    if (typeof secret !== "string" || !secret.startsWith("twhsec_")) {
      throw new Error("Missing signing secret");
    }

    const match = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(signatureHeader);
    if (!match) throw new Error("Malformed signature");

    const timestamp = Number(match[1]);
    if (!Number.isSafeInteger(timestamp) || Math.abs(now / 1000 - timestamp) > 300) {
      throw new Error("Expired signature");
    }

    const expected = createHmac("sha256", Buffer.from(secret, "utf8"))
      .update(`${match[1]}.`, "utf8")
      .update(rawBody)
      .digest();
    const received = Buffer.from(match[2], "hex");
    if (!timingSafeEqual(expected, received)) throw new Error("Invalid signature");

    return JSON.parse(rawBody.toString("utf8"));
  }
  ```

  * Jako klíč v UTF-8 použijte celý řetězec `twhsec_...`. Klíč nedekódujte a neodstraňujte jeho prefix.
  * Odmítněte časová razítka vzdálená od vašich hodin více než pět minut a udržujte hodiny synchronizované.
  * Během [obměny podpisového klíče](/webhooks/configuration#configuration-changes) nejprve zkuste nový klíč, poté starý, dokud se fronta nevyprázdní.

  ### Uložte a potvrďte [#store-and-acknowledge]

  Událost potvrďte až po jejím bezpečném uložení, aby chyba na vaší straně vyvolala opakovaný pokus.

  ```js title="server.mjs"
  import express from "express";
  import { verifyTapayaWebhook } from "./verify-tapaya-webhook.mjs";
  import { saveEventOnce } from "./events.mjs"; // your store, unique on event id

  const app = express();

  app.post(
    "/webhooks/tapaya",
    express.raw({ type: "application/json" }),
    async (req, res) => {
      let event;
      try {
        event = verifyTapayaWebhook(
          req.body,
          req.get("X-Tapaya-Signature"),
          process.env.TAPAYA_WEBHOOK_SECRET,
        );
      } catch {
        return res.sendStatus(400);
      }
      if (event.organizationId !== process.env.TAPAYA_ORGANIZATION_ID) {
        return res.sendStatus(400);
      }

      await saveEventOnce(event); // skips an id it already stored
      res.sendStatus(204);
    },
  );
  ```

  * Pro událost, kterou už máte uloženou, vraťte `2xx`. Opakované pokusy zachovávají stejné `id` i tělo, ale mají nové časové razítko a podpis.
  * Pokud novou událost nelze uložit, vraťte odpověď mimo `2xx`, aby se doručení opakovalo.
  * Uložené události zpracovávejte z vlastní fronty. Pořadí doručení není zaručené, proto starší snímky porovnávejte s aktuálním stavem.
</div>

## Odešlete testovací webhook [#send-a-test-webhook]

Testovací akce v Platform odešle jeden podepsaný požadavek `webhook.test` na uloženou URL. Funguje i tehdy, když jsou webhooky vypnuté nebo typ není ve vašem odběru, a nevytváří žádnou platbu, vratku ani obchodníka.

1. Otevřete **API Keys** své organizace a uložte cílovou adresu.
2. Použijte testovací akci webhooku.
3. Zkontrolujte výsledek, dobu trvání a odeslaný JSON, které Platform zobrazí.

```json
{
  "id": "019a0000-0000-7000-8000-000000000010",
  "type": "webhook.test",
  "apiVersion": "1",
  "organizationId": "019a0000-0000-7000-8000-000000000002",
  "merchantId": null,
  "createdAt": "2026-10-30T12:00:00.000000Z",
  "test": true,
  "data": {
    "message": "This is a test webhook from Tapaya. No payment was created."
  }
}
```

<Accordions type="multiple">
  <Accordion title="Co test pokrývá" id="what-a-test-covers">
    * Jeden pokus s limitem 10 sekund a bez automatického opakování.
    * Ověřuje doručení a práci s podpisem, nikoli payloady plateb či onboardingu. Ty otestujte pomocí aktivit v sandboxu.
    * Neúspěšné doručení se zobrazí jako výsledek testu, nikoli jako chyba v Platform.
  </Accordion>

  <Accordion title="Limity testů" id="test-limits">
    * Doručení z fronty a ruční testy sdílejí limit 2 současných požadavků na organizaci.
    * Ruční testy navíc umožňují 10 zahájení za klouzavých 60 sekund na organizaci, společně pro všechny uživatele.
    * Při překročení kteréhokoli limitu test vrátí `429` s `Retry-After` v sekundách a nic neodešle.
    * Přijaté testy se započítávají i při chybě nebo zrušení. Opětovné vytvoření cílové adresy počet neobnoví.
  </Accordion>
</Accordions>

## Lokální a automatizační příjemci [#local-and-automation-receivers]

Když váš produkční server ještě není připravený, nasměrujte webhook na vývojový počítač nebo no-code nástroj.

<Accordions type="multiple">
  <Accordion title="Otestujte lokálního příjemce" id="test-a-local-receiver">
    * Jako cílovou adresu uložte veřejnou HTTPS URL tunelu; localhost adresy a privátní IP adresy se odmítají.
    * Ponechte zapnuté ověřování podpisu a zajistěte, aby tunel předával původní tělo a hlavičky Tapaya beze změny.
    * Když se URL tunelu změní, cílovou adresu znovu uložte. Starý tunel ponechte dostupný, dokud se události, které na něj čekají ve frontě, nevyprázdní.
  </Accordion>

  <Accordion title="Připojte automatizační nástroj" id="connect-an-automation-tool">
    * Vstupní bod nesmí potřebovat vlastní autentizační hlavičky, musí zachovat původní tělo a odpovědět do 10 sekund.
    * Musí sám ověřit HMAC původního těla, nebo být za ověřovacím adaptérem, který to dělá.
    * Jakmile nástroj odpoví `2xx`, doručení se počítá jako úspěšné. Následné chyby workflow a duplicity musí řešit nástroj; další doručení nespustí.
  </Accordion>
</Accordions>

## Řešení chyb [#handle-errors]

<Accordions type="multiple">
  <Accordion title="Výsledky testu" id="test-results">
    | Výsledek          | Význam                                                              |
    | ----------------- | ------------------------------------------------------------------- |
    | `delivered: true` | Příjemce vrátil HTTP `2xx`.                                         |
    | `http_error`      | Příjemce vrátil stav mimo `2xx`.                                    |
    | `transport_error` | Cíl nebyl dostupný, včetně chyb TLS, DNS nebo ověření veřejné sítě. |
    | `timeout`         | Příjemce nepotvrdil přijetí do 10 sekund.                           |
  </Accordion>

  <Accordion title="Kontroly podpisu" id="signature-checks">
    | Příznak         | Co zkontrolovat                                                                                            |
    | --------------- | ---------------------------------------------------------------------------------------------------------- |
    | Neshoda podpisu | Použijte celý klíč a původní bajty. Přeformátovaný JSON nebo API token použitý jako klíč ověření neprojde. |
    | Prošlý podpis   | Synchronizujte hodiny příjemce a ověřujte podle hlavičky požadavku, nikoli `createdAt`.                    |
  </Accordion>

  <Accordion title="Doručování" id="delivery">
    | Příznak                                     | Co zkontrolovat                                                                                                                                   |
    | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
    | Odmítnutá URL nebo blokované připojení      | Použijte veřejné HTTPS na portu 443 bez přesměrování, přihlašovacích údajů či fragmentu.                                                          |
    | Žádná událost webhooku                      | Zkontrolujte organizaci, zapnutí a vybrané typy událostí. Minulé změny se zpětně nedoplňují.                                                      |
    | Duplicitní nebo starší událost              | Odstraňujte duplicity podle `id` a porovnávejte s aktuálním stavem místo předpokladu pořadí. Viz [pravidla doručování](/webhooks#delivery-rules). |
    | Test funguje, ale zpracování platby selhává | `webhook.test` má jiný tvar `data`. Rozhodujte podle `type` a testujte se skutečnými událostmi v sandboxu.                                        |
  </Accordion>
</Accordions>
