> ## Documentation Index
> Fetch the complete documentation index at: https://devdocs.paywithatoa.co.uk/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](/api-reference/Webhook/v2-signing) for setup instructions.
</Note>

***

## 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.

<CodeGroup>
  ```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';
  ```
</CodeGroup>

***

## 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](/api-reference/Webhook/v2-signing) 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.
</Note>
