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

# V1 Webhook Signing (Legacy)

> How to verify Atoa v1 webhook signatures so you can trust incoming events, with the signing scheme and example code.

V1 signing verifies webhook authenticity using a `signatureHash` field in the JSON body, signing only the `orderId` combined with `paymentRequestId` (payments) or `refundId` (refunds).

> [!NOTE]
>
> **V2 signing is recommended for all new integrations.** V2 signs the entire request body and uses a dedicated signing secret. See the [V2 Webhook Signing guide](https://docs.paywithatoa.com/api-reference/webhook/v2-signing.md) for setup instructions.

***

## How v1 signature verification works

| | Details |
| - | - |
| **Signature location** | `signatureHash` field in the JSON body |
| **What is signed** | `orderId \| paymentRequestId` (payments) or `orderId \| refundId` (refunds) |
| **Algorithm** | HMAC-SHA256 |
| **Key** | Your Atoa API secret (the same secret used for API authentication) |

***

## Payment signature verification

To verify a payment webhook:

1. **Retrieve the `orderId`** from your server — the one you passed when creating the payment request (not `atoaOrderId`).
2. **Get the `paymentRequestId`** from the webhook payload.
3. **Compute** `HMAC-SHA256(orderId + "|" + paymentRequestId, atoaSecret)`.
4. **Compare** the result against the `signatureHash` field in the payload.

```javascript Node.js theme={null}
const crypto = require('crypto');

function verifyV1Signature(orderId, paymentRequestId, atoaSecret, signatureHash) {
  const data = orderId + '|' + paymentRequestId;
  const generatedSignature = crypto
    .createHmac('sha256', atoaSecret)
    .update(data)
    .digest('hex');

  return generatedSignature === signatureHash;
}

// Usage
app.post('/webhook', express.json(), (req, res) => {
  const { orderId, paymentRequestId, signatureHash } = req.body;
  const yourOrderId = getOrderIdFromYourServer(); // Your original orderId, not atoaOrderId

  if (!verifyV1Signature(yourOrderId, paymentRequestId, process.env.ATOA_SECRET, signatureHash)) {
    return res.status(401).send('Invalid signature');
  }

  // Process the event...
  res.status(200).send('OK');
});
```

```python Python theme={null}
import hmac
import hashlib

def verify_v1_signature(order_id, payment_request_id, atoa_secret, signature_hash):
    data = f"{order_id}|{payment_request_id}"
    generated_signature = hmac.new(
        atoa_secret.encode("utf-8"),
        data.encode("utf-8"),
        hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(generated_signature, signature_hash)

# Flask example
@app.route("/webhook", methods=["POST"])
def webhook():
    event = request.get_json()
    your_order_id = get_order_id_from_your_server()  # Your original orderId, not atoaOrderId

    if not verify_v1_signature(
        your_order_id, event["paymentRequestId"], ATOA_SECRET, event["signatureHash"]
    ):
        return "Invalid signature", 401

    # Process the event...
    return "OK", 200
```

```java Java theme={null}
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.MessageDigest;

public class V1WebhookVerifier {

    private static final String HMAC_SHA256 = "HmacSHA256";

    public static boolean verifySignature(
            String orderId, String paymentRequestId,
            String atoaSecret, String signatureHash) {

        String data = orderId + "|" + paymentRequestId;

        try {
            SecretKeySpec keySpec = new SecretKeySpec(
                atoaSecret.getBytes("UTF-8"), HMAC_SHA256);
            Mac mac = Mac.getInstance(HMAC_SHA256);
            mac.init(keySpec);

            byte[] hash = mac.doFinal(data.getBytes("UTF-8"));
            StringBuilder hex = new StringBuilder();
            for (byte b : hash) {
                hex.append(String.format("%02x", b));
            }

            return MessageDigest.isEqual(
                signatureHash.getBytes(), hex.toString().getBytes());
        } catch (Exception e) {
            return false;
        }
    }
}
```

```go Go theme={null}
package main

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

func verifyV1Signature(orderId, paymentRequestId, atoaSecret, signatureHash string) bool {
	data := orderId + "|" + paymentRequestId

	mac := hmac.New(sha256.New, []byte(atoaSecret))
	mac.Write([]byte(data))
	generatedSignature := hex.EncodeToString(mac.Sum(nil))

	return hmac.Equal([]byte(generatedSignature), []byte(signatureHash))
}
```

```php PHP theme={null}
<?php

function verifyV1Signature(
    string $orderId, string $paymentRequestId,
    string $atoaSecret, string $signatureHash
): bool {
    $data = $orderId . '|' . $paymentRequestId;
    $generatedSignature = hash_hmac('sha256', $data, $atoaSecret);

    return hash_equals($generatedSignature, $signatureHash);
}

// Usage
$event = json_decode(file_get_contents('php://input'), true);
$yourOrderId = getOrderIdFromYourServer(); // Your original orderId, not atoaOrderId

if (!verifyV1Signature($yourOrderId, $event['paymentRequestId'], $atoaSecret, $event['signatureHash'])) {
    http_response_code(401);
    echo 'Invalid signature';
    exit;
}

// Process the event...
http_response_code(200);
echo 'OK';
```

***

## Refund signature verification

For refund webhooks, the signature uses `refundId` instead of `paymentRequestId`:

```
hmac_sha256(orderId + "|" + refundId, atoaSecret)
```

The verification logic is identical — just substitute `paymentRequestId` with `refundId` from the refund webhook payload.

```javascript Node.js theme={null}
function verifyRefundSignature(orderId, refundId, atoaSecret, signatureHash) {
  const data = orderId + '|' + refundId;
  const generatedSignature = crypto
    .createHmac('sha256', atoaSecret)
    .update(data)
    .digest('hex');

  return generatedSignature === signatureHash;
}
```

***

## Why V2 is recommended

V2 signing improves on V1 in several ways:

* **Full-body coverage** — V2 signs the entire request body, protecting all fields in the payload.
* **Dedicated signing secret** — V2 uses a separate signing secret rather than your API key, so your credentials stay compartmentalised.
* **POS support** — `POS_PAYMENT_STATUS` webhooks require V2 signing.

***

## Migration to V2

To migrate from V1 to V2 signing:

1. Generate a signing secret from the [Atoa Dashboard](https://dashboard.paywithatoa.co.uk/my-account/settings/webhooks) under **Settings → Webhooks**.
2. Update your webhook verification logic to use the `X-Atoa-Signature` header instead of the `signatureHash` body field.
3. Once a signing secret is generated, the `signatureHash` and `signature` fields are removed from webhook payloads and an `eventType` field is added.

See the [V2 Webhook Signing guide](https://docs.paywithatoa.com/api-reference/webhook/v2-signing.md) for full setup instructions and code samples.

> [!NOTE]
>
> V1 and V2 cannot be used simultaneously for the same merchant. Generating a signing secret switches all your webhook deliveries to V2.


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