> ## Documentation Index
> Fetch the complete documentation index at: https://docs.paywithsoap.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Overview

We send you the following webhooks:

* `checkout.succeeded` - a payment was successful
* `checkout.failed` - a payment failed
* `checkout.hold` - during withdrawal, we ask you to place a hold on the available balance to prevent double spending and verify balance is sufficient.
* `checkout.release_hold` - a withdrawal failed after you placed a hold.
* `checkout.returned` - a payment was returned
* `checkout.pending` - a checkout session has entered a pending state
* `checkout.terminally_failed` - a checkout failed due to geo, kyc, or other fraud checks.
* `checkout.expired` - a checkout expired (for security purposes all checkouts expire)
* `checkout.voided` - a deposit charge was voided after it had succeeded
* `checkout.review.created` *(optional)* - a payment was flagged for review
* `checkout.review.approved` *(optional)* - a customer support agent approved a review
* `checkout.review.declined` *(optional)* - a customer support agent declined a review

## Verifying Webhook Signatures

In order to prevent mailicious actors, we include a signature in the webhook event as a header`SOAP-WEBHOOK-SIGNATURE`. This signature was generated using your webhook signing secret that you can access in your dashboard in the Developers sections.

The `SOAP-WEBHOOK-SIGNATURE` header has the following format:

```
t=1673979302,v1=6ffbb59b2300aae63f272406069a9788598b792a944a07aba816edb039989a39
```

`t` is the timestamp when the signature was generated and should be used to prevent replay attacks.

`v1` is the signature itself, generated using HMAC-SHA256.

In order to verify the signature, you will use the event payload and recreate the signature using your webhook signing secret, and compare it to the signature you received.

```typescript theme={null}
import crypto from 'crypto';

function verifySignature(payload: string, signatureHeader: string, signingSecret: string): boolean {
  const parts = signatureHeader.split(",");
  const timestampPart = parts.find((p) => p.startsWith("t="));
  const signaturePart = parts.find((p) => p.startsWith("v1="));

  if (!timestampPart || !signaturePart) {
      return false;
  }

  const timestamp = timestampPart.split("=")[1];
  const receivedSignature = signaturePart.split("=")[1];

  // Create the signature message
  const message = `${timestamp}.${payload}`;
  
  // Calculate signature
  const calculatedSignature = crypto
      .createHmac("sha256", signingSecret)
      .update(message)
      .digest("hex");

  // Compare signatures using a timing-safe comparison
  return crypto.timingSafeEqual(
      Buffer.from(receivedSignature, "hex"),
      Buffer.from(calculatedSignature, "hex")
  );
}

```

If the signatures dont match you should reject the webhook and not process it.

## Updating Your Internal Ledger

It depends on how you implement your accounting system. In this [example](https://github.com/Soap-Payments/sample-code/blob/main/webhooks.js), we only use credits and debits and update the customer's balance or internal ledger accordingly.

## Other

* We retry webhooks 4 times after the first attempt with the exception of `checkout.hold`.
* There is a 10 second timeout.
* Each webhook comes with `event_id` — use this as your idempotency key to not reprocess events.
