# WEBHOOK description

> Understand Issuing webhook headers, payload envelope, merchant acknowledgement, and HMAC-SHA256 signature verification.

Use this page before implementing Issuing webhook callbacks. Onerway pushes card operation, card transaction, and 3DS events to the callback URL configured for the merchant.

## Request headers

| Header | Type | Required | Description |
| --- | --- | --- | --- |
| `Content-Type` | String | Yes | Fixed to `application/json;charset=UTF-8`. |
| `x-timestamp` | String | Yes | Unix timestamp in seconds. |
| `x-signature` | String | Yes | Generate the HMAC-SHA256 signature from `webhook_secret` and `x-timestamp + "." + raw_body`. See [Signature and verification](/issuing/api-reference/webhook-description#signature-and-verification). |

## Payload structure

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `request_id` | String | Yes | Unique event identifier for deduplication. |
| `event_type` | String | Yes | Event type. Issuing currently sends `issuing.cardOperateEvent`, `issuing.cardTransactionEvent`, and `issuing.card3dsEvent`. |
| `created_at` | String | Yes | Event creation time in ISO 8601 format. |
| `version` | String | No | Webhook API version selected when the subscription is created. Defaults to `1.0` when not specified. |
| `data` | Object | Yes | Business data. The structure depends on `event_type`. |

## Merchant response requirements

The merchant callback must return a JSON response. Onerway uses this response to decide whether the push was received successfully.

| Field | Type | Description |
| --- | --- | --- |
| `respCode` | String | Return `20000` to confirm receipt. |
| `respMsg` | String | Response message. |

If the callback response code is not `20000`, Onerway retries every 15 seconds, up to 10 times.

## Signature and verification

Onerway signs asynchronous notifications such as card operation, transaction, and 3DS events. Verify the signature before processing the event.

Build the signed content as:

```text
x-timestamp + "." + raw_body
```

Generate the expected signature with HMAC-SHA256 and compare it with `x-signature` case-insensitively.

<code-collapse name="Signature examples">
<code-group sync="issuing-webhook-signing-lang">

```java [Java]
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Base64;
import java.util.HexFormat;

public final class HmacSHA256Util {
    private HmacSHA256Util() {
    }

    public static String signHmacSHA256(String webhook_secret, String content) throws Exception {
        byte[] key = Base64.getDecoder().decode(webhook_secret);
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(key, "HmacSHA256"));
        return HexFormat.of().formatHex(mac.doFinal(content.getBytes(StandardCharsets.UTF_8)));
    }

    public static boolean verifyHmacSHA256(String webhook_secret, String content, String signed) throws Exception {
        String expected = signHmacSHA256(webhook_secret, content);
        return MessageDigest.isEqual(
            expected.toLowerCase().getBytes(StandardCharsets.UTF_8),
            signed.toLowerCase().getBytes(StandardCharsets.UTF_8)
        );
    }
}
```

```python [Python]
import base64
import hashlib
import hmac

def sign_hmac_sha256(webhook_secret: str, content: str) -> str:
    key = base64.b64decode(webhook_secret)
    return hmac.new(key, content.encode("utf-8"), hashlib.sha256).hexdigest()

def verify_hmac_sha256(webhook_secret: str, content: str, signed: str) -> bool:
    expected = sign_hmac_sha256(webhook_secret, content)
    return hmac.compare_digest(expected.lower(), signed.lower())
```

```php [PHP]
<?php

final class HmacSHA256Util
{
    public static function signHmacSHA256(string $webhook_secret, string $content): string
    {
        $key = base64_decode($webhook_secret, true);
        if ($key === false) {
            throw new InvalidArgumentException('Invalid webhook secret.');
        }

        return hash_hmac('sha256', $content, $key);
    }

    public static function verifyHmacSHA256(string $webhook_secret, string $content, string $signed): bool
    {
        $expected = self::signHmacSHA256($webhook_secret, $content);
        return hash_equals(strtolower($expected), strtolower($signed));
    }
}
```

</code-group>
</code-collapse>
