# Verifica e test (/it/webhooks/receiving)



Verifichi ogni webhook prima di utilizzarne il payload, quindi invii un test per confermare che il ricevitore funzioni dall'inizio alla fine.

## Prima di iniziare [#before-you-start]

* Una [destinazione salvata](/webhooks/configuration#save-a-destination) e il relativo signing secret (chiave di firma), `twhsec_...`. Non utilizzi il Server Secret Token per verificare i webhook.
* Una route del ricevitore in grado di leggere il corpo originale della richiesta prima che un parser JSON lo modifichi.
* Un archivio con un vincolo di unicità sull'`id` dell'evento, così che i duplicati siano gestiti in modo sicuro.

## Ricevere un webhook [#receive-a-webhook]

<div className="fd-steps [&_h3]:fd-step">
  ### Leggere il corpo originale [#read-the-raw-body]

  Tapaya invia una richiesta `POST` JSON con questi header. Mantenga il corpo come byte originali; un corpo sottoposto a parsing e serializzato di nuovo non corrisponde più alla firma.

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

  * Con Express, utilizzi `express.raw({ type: "application/json" })` per questa route, prima di un parser JSON globale.

  ### Controllare la firma [#check-the-signature]

  Calcoli HMAC-SHA256 sul timestamp, un punto e il corpo originale, quindi lo confronti con `v1` tramite un confronto a tempo costante.

  ```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"));
  }
  ```

  * Utilizzi il segreto completo `twhsec_...` come chiave UTF-8. Non lo decodifichi e non ne rimuova il prefisso.
  * Rifiuti i timestamp che differiscono di oltre cinque minuti dal Suo orologio e mantenga l'orologio sincronizzato.
  * Durante la [rotazione del signing secret](/webhooks/configuration#configuration-changes), provi prima il nuovo segreto, poi quello precedente, finché la coda non si è svuotata.

  ### Memorizzare e confermare [#store-and-acknowledge]

  Accetti l'evento solo dopo averlo memorizzato in modo sicuro, così che un errore da parte Sua generi un nuovo tentativo.

  ```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);
    },
  );
  ```

  * Restituisca `2xx` per un evento già memorizzato. I tentativi successivi mantengono lo stesso `id` e lo stesso corpo, con un timestamp e una firma nuovi.
  * Restituisca una risposta diversa da `2xx` quando non può memorizzare un nuovo evento, così che la consegna riprovi.
  * Elabori gli eventi memorizzati dalla Sua coda. L'ordine di consegna non è garantito, quindi riconcili le istantanee meno recenti con lo stato corrente.
</div>

## Inviare un webhook di test [#send-a-test-webhook]

L'azione di test della Platform invia una richiesta `webhook.test` firmata all'URL salvato. Funziona anche se i webhook sono disabilitati o il tipo non è incluso nella Sua iscrizione, e non crea alcun pagamento, rimborso o esercente.

1. Apra **API Keys** per la Sua organizzazione e salvi la destinazione.
2. Utilizzi l'azione di test del webhook.
3. Controlli il risultato, la durata e il JSON inviato mostrati nella Platform.

```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="Cosa copre un test" id="what-a-test-covers">
    * Un solo tentativo, con un limite di 10 secondi e senza tentativi automatici successivi.
    * Verifica la consegna e la gestione della firma, non i payload di pagamento o onboarding. Li testi con attività nella sandbox.
    * Una consegna fallita viene mostrata come risultato del test, non come errore nella Platform.
  </Accordion>

  <Accordion title="Limiti dei test" id="test-limits">
    * Le consegne in coda e i test manuali condividono il limite di 2 richieste contemporanee per organizzazione.
    * I test manuali consentono inoltre 10 avvii in una finestra mobile di 60 secondi per organizzazione, condivisa da tutti gli utenti.
    * Oltre uno dei due limiti, il test restituisce `429` con `Retry-After` in secondi e non invia nulla.
    * I test ammessi vengono conteggiati anche in caso di errore o annullamento. Ricreare la destinazione non azzera il conteggio.
  </Accordion>
</Accordions>

## Ricevitori locali e di automazione [#local-and-automation-receivers]

Indirizzi un webhook verso una macchina di sviluppo o uno strumento no-code quando il Suo server di produzione non è ancora pronto.

<Accordions type="multiple">
  <Accordion title="Testare un ricevitore locale" id="test-a-local-receiver">
    * Salvi come destinazione l'URL di un tunnel HTTPS pubblico; gli URL localhost e gli indirizzi IP privati vengono rifiutati.
    * Mantenga abilitata la verifica della firma e si assicuri che il tunnel inoltri il corpo originale e gli header Tapaya senza modificarli.
    * Quando l'URL del tunnel cambia, salvi nuovamente la destinazione. Mantenga attivo il vecchio tunnel finché gli eventi in coda verso di esso non si sono svuotati.
  </Accordion>

  <Accordion title="Collegare uno strumento di automazione" id="connect-an-automation-tool">
    * Il punto di ingresso non deve richiedere header di autenticazione personalizzati, deve mantenere il corpo originale e rispondere entro 10 secondi.
    * Deve controllare autonomamente l'HMAC del corpo originale oppure trovarsi dietro un adattatore di verifica che lo fa.
    * Una volta che lo strumento risponde `2xx`, la consegna è considerata riuscita. Gli errori successivi del flusso di lavoro e i duplicati sono gestiti dallo strumento; non attivano un'altra consegna.
  </Accordion>
</Accordions>

## Gestire gli errori [#handle-errors]

<Accordions type="multiple">
  <Accordion title="Risultati dei test" id="test-results">
    | Risultato         | Significato                                                                                                    |
    | ----------------- | -------------------------------------------------------------------------------------------------------------- |
    | `delivered: true` | Il ricevitore ha restituito HTTP `2xx`.                                                                        |
    | `http_error`      | Il ricevitore ha restituito uno stato diverso da `2xx`.                                                        |
    | `transport_error` | Non è stato possibile raggiungere la destinazione, inclusi errori TLS, DNS o di convalida della rete pubblica. |
    | `timeout`         | Il ricevitore non ha confermato la ricezione entro 10 secondi.                                                 |
  </Accordion>

  <Accordion title="Controlli della firma" id="signature-checks">
    | Sintomo                  | Verifica                                                                                                                              |
    | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- |
    | Firma non corrispondente | Utilizzi il segreto completo e i byte originali. Il JSON riformattato, oppure il token API usato come chiave, fa fallire la verifica. |
    | Firma scaduta            | Sincronizzi l'orologio del ricevitore e verifichi rispetto all'header della richiesta, non a `createdAt`.                             |
  </Accordion>

  <Accordion title="Consegna" id="delivery">
    | Sintomo                                                | Verifica                                                                                                                                            |
    | ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- |
    | URL rifiutato o connessione bloccata                   | Utilizzi HTTPS pubblico sulla porta 443 senza reindirizzamenti, credenziali o frammento.                                                            |
    | Nessun evento webhook                                  | Verifichi l'organizzazione, lo stato di abilitazione e i tipi di evento selezionati. Le modifiche passate non vengono recuperate retroattivamente.  |
    | Evento duplicato o meno recente                        | Deduplichi tramite `id` e riconcili con lo stato corrente anziché presumere un ordine. Si vedano le [regole di consegna](/webhooks#delivery-rules). |
    | Il test funziona ma la gestione del pagamento fallisce | `webhook.test` ha una struttura `data` diversa. Instradi in base a `type` e testi con eventi reali della sandbox.                                   |
  </Accordion>
</Accordions>
