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

# Handle Webhooks

> Configure, verify, and process bluSwap webhook events for collections, refunds, and payouts with retry-safe patterns.

bluSwap posts webhook events to your configured callback URL when transaction states change. This guide covers configuration, signature verification, and idempotent handling.

## Configure your endpoint

Set your callback URL in the bluSwap dashboard before processing live transactions. See [Collections webhooks](/docs/collections/webhook-configuration.md) and [Payouts webhooks](/docs/payouts/webhook-configuration.md) for supported event types.

## Verify the signature

Every webhook includes a signature header. Compare it against an HMAC of the raw request body using your webhook secret:

```sh Shell theme={"dark"}
# Verify signature with openssl (reads raw body from stdin)
EXPECTED=$(printf '%s' "$RAW_BODY" | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" | awk '{print $2}')
[ "$EXPECTED" = "$SIGNATURE_HEADER" ] && echo "valid" || echo "invalid"
```

```js Node.js theme={"dark"}
import crypto from "crypto";

function verify(rawBody, signature, secret) {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(rawBody)
    .digest("hex");
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signature)
  );
}
```

```java Java theme={"dark"}
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;

public static boolean verify(byte[] rawBody, String signature, String secret) throws Exception {
    Mac mac = Mac.getInstance("HmacSHA256");
    mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
    byte[] hash = mac.doFinal(rawBody);
    StringBuilder hex = new StringBuilder();
    for (byte b : hash) hex.append(String.format("%02x", b));
    return MessageDigest.isEqual(
        hex.toString().getBytes(StandardCharsets.UTF_8),
        signature.getBytes(StandardCharsets.UTF_8)
    );
}
```

```python Python theme={"dark"}
import hmac, hashlib

def verify(raw_body: bytes, signature: str, secret: str) -> bool:
    expected = hmac.new(
        secret.encode(), raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)
```

```go Go theme={"dark"}
package webhooks

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
)

func Verify(rawBody []byte, signature, secret string) bool {
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write(rawBody)
	expected := hex.EncodeToString(mac.Sum(nil))
	return hmac.Equal([]byte(expected), []byte(signature))
}
```

```php PHP theme={"dark"}
<?php
function verify(string $rawBody, string $signature, string $secret): bool {
    $expected = hash_hmac('sha256', $rawBody, $secret);
    return hash_equals($expected, $signature);
}
```

```ruby Ruby theme={"dark"}
require "openssl"

def verify(raw_body, signature, secret)
  expected = OpenSSL::HMAC.hexdigest("sha256", secret, raw_body)
  Rack::Utils.secure_compare(expected, signature)
end
```

## Respond quickly

Return a `2xx` response within 5 seconds. Queue heavy work (database writes, downstream API calls, emails) to a background job so bluSwap does not retry.

## Handle retries idempotently

bluSwap retries failed deliveries with exponential backoff. Deduplicate using the `event_id`:

**1. Store processed event IDs**

On receipt, check if the `event_id` already exists in your database.

**2. Skip duplicates**

If it exists, return `200` immediately without reprocessing.

**3. Persist before ack**

Only acknowledge after your business logic and the event ID are committed to the database.

> [!NOTE]
>
> Test your endpoint with the sandbox environment before enabling production webhooks.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.