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

# Checkout Webhook

> Webhook payload schema, signature verification, retry behaviour, and idempotency.

**AllScale Open API**\
**Version:** v6\
**Last Updated:** 2026-09-09

***

## Overview

AllScale delivers event notifications to your server via **Webhook
callbacks**.

Each webhook request is authenticated using **HMAC-SHA256 request
signing**, providing:

* Sender authentication (shared secret)
* Payload integrity (tamper-proof)
* Replay attack protection (timestamp + nonce)
* Stateless verification (no session required)

Webhook payloads are plaintext JSON (not encrypted).

***

## Credentials

When your integration/store is created, you receive:

| Field       | Description                                |
| ----------- | ------------------------------------------ |
| api\_key    | Public identifier                          |
| api\_secret | Secret key used for signature verification |

### Important Notes

* `api_secret` is shown only once
* It cannot be retrieved again
* Store it securely
* Treat it like a password or private key

***

## Webhook Request

### Method

```http theme={null}
POST
```

### Content-Type

```http theme={null}
application/json
```

### Required Headers

| Header              | Description              |
| ------------------- | ------------------------ |
| X-API-Key           | API key                  |
| X-Webhook-Id        | Unique webhook ID        |
| X-Webhook-Timestamp | Unix timestamp(seconds)  |
| X-Webhook-Nonce     | Unique per-request nonce |
| X-Webhook-Signature | HMAC signature           |

***

## Signature Header Format

```http theme={null}
X-Webhook-Signature: v1=<signature>
```

***

## Replay Protection

To prevent replay attacks:

### Requirements

* Timestamp must be within **±5 minutes**
* Each nonce must be used **only once**

### Recommended Implementation

* Store `nonce` in Redis
* TTL: **600 seconds**
* Reject duplicate nonces

***

## Canonical String (v1)

Webhook signatures are generated using a canonical string.

### Canonical Format

```text theme={null}
allscale:webhook:v1
METHOD
PATH
QUERY_STRING
WEBHOOK_ID
TIMESTAMP
NONCE
BODY_SHA256
```

***

## Field Description

| Field         | Description                  |
| ------------- | ---------------------------- |
| METHOD        | HTTP method (uppercase)      |
| PATH          | URL path only                |
| QUERY\_STRING | Query string without `?`     |
| WEBHOOK\_ID   | From `X-Webhook-Id`          |
| TIMESTAMP     | From `X-Webhook-Timestamp`   |
| NONCE         | From `X-Webhook-Nonce`       |
| BODY\_SHA256  | SHA256 hex of raw body bytes |

⚠️ **Important**

`BODY_SHA256` must be calculated from the **raw request body bytes**,\
**before JSON parsing or re-serialization**.

***

## Signature Algorithm

### Algorithm

```text theme={null}
HMAC-SHA256
```

### Encoding

```text theme={null}
Base64
```

### Formula

```text theme={null}
signature = Base64(
  HMAC_SHA256(api_secret, canonical_string_bytes)
)
```

***

## Header Example

```
X-Webhook-Signature: v1=<signature>
```

***

# Webhook Payload

The request body sent by AllScale is **exactly** the following JSON
structure.

## JSON Field Definitions

| Field                            | Type            | Required | Description                                                                                                                                                                                                                                                                                                                          |
| -------------------------------- | --------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| all\_scale\_transaction\_id      | string          | ✅        | AllScale transaction ID for this payment/transfer                                                                                                                                                                                                                                                                                    |
| all\_scale\_checkout\_intent\_id | string          | ✅        | AllScale checkout intent ID associated with the payment                                                                                                                                                                                                                                                                              |
| webhook\_id                      | string          | ✅        | Unique webhook ID (must match `X-Webhook-Id`)                                                                                                                                                                                                                                                                                        |
| amount\_cents                    | integer         | ✅        | Always the integer the merchant submitted on `POST /v1/checkout_intents/`. For fiat-priced intents this is fiat cents; for stable-coin-priced intents it is stable-coin "cents" (1.00 coin = 100 cents, equivalent to `int(amount_coins * 100)`).                                                                                    |
| currency                         | integer \| null | ➖        | Currency enum value (int). Must be interpreted using AllScale Currency enum mapping. `null` when the intent was priced natively in a stable coin — read `coin_symbol` instead.                                                                                                                                                       |
| currency\_symbol                 | string \| null  | ➖        | Fiat currency symbol (e.g., `USD`, `CAD`, `CNY`). `null` when the intent was priced natively in a stable coin — read `coin_symbol` instead.                                                                                                                                                                                          |
| amount\_coins                    | string          | ✅        | Stablecoin amount as a **decimal string** (to avoid float issues), e.g. `"12.340000"`                                                                                                                                                                                                                                                |
| coin\_contract\_address          | string          | ✅        | Official ERC-20 token contract address                                                                                                                                                                                                                                                                                               |
| coin\_symbol                     | string          | ✅        | Stablecoin symbol (e.g., `USDT`, `USDC`)                                                                                                                                                                                                                                                                                             |
| chain\_id                        | integer         | ✅        | EIP-155 chainId identifying the EVM network ([https://chainid.network/](https://chainid.network/))                                                                                                                                                                                                                                   |
| tx\_hash                         | string          | ✅        | On-chain transaction hash                                                                                                                                                                                                                                                                                                            |
| tx\_from                         | string          | ✅        | Sender wallet address                                                                                                                                                                                                                                                                                                                |
| payment\_method\_type            | integer         | ✅        | Payment method type used for this transaction. Value must correspond to the `PaymentMethodType` enum: `0=UNKNOWN`, `1=WALLET_SCAN`, `2=WALLET_CONNECT`, `3=ALL_SCALE_PAY`.                                                                                                                                                           |
| user\_id                         | string \| null  | ➖        | Optional merchant/user identifier                                                                                                                                                                                                                                                                                                    |
| order\_id                        | string \| null  | ➖        | Optional merchant order identifier                                                                                                                                                                                                                                                                                                   |
| user\_name                       | string \| null  | ➖        | Optional customer/user display name                                                                                                                                                                                                                                                                                                  |
| extra\_obj                       | object \| null  | ➖        | Optional arbitrary JSON object with extra fields                                                                                                                                                                                                                                                                                     |
| status                           | integer \| null | ➖        | Settlement state of the checkout at the moment the callback was built. `20` = confirmed (paid). **Check `status == 20` before treating an order as paid** — do not infer payment from the mere arrival of a callback. `null` only on callbacks emitted before this field existed. See [Verifying a callback](#verifying-a-callback). |
| actual\_paid\_amount             | string \| null  | ➖        | Amount the **buyer paid**, as a decimal string in the same unit as `amount_coins`. This is not necessarily the amount credited to your wallet — see [Verifying a callback](#verifying-a-callback). `null` when it was not recorded.                                                                                                  |

## Verifying a callback

Two fields let you confirm a payment from the callback itself rather than
assuming that receiving one means "paid".

**`status` — check it before marking an order paid.**
A callback is only ever *built* on confirmed settlement, so in practice `status`
is `20`. Check it anyway: it is the field that makes the assumption explicit, and
an integration that defaults a missing or unexpected status to "paid" will mark
unpaid orders as paid. Treat anything other than `20` as *not paid*, and treat
`null` the same way unless you are knowingly processing a historical callback
from before this field shipped.

**`actual_paid_amount` — it is the buyer's payment, not always your credit.**
Read it as "what the buyer sent", and note that the two are not always equal:

* When the buyer pays your wallet directly, no platform fee is deducted along
  the way, so this equals the amount credited to you.
* When the payment is routed through an AllScale temporary wallet, this is the
  buyer's **gross** deposit. AllScale deducts the platform fee before settling to
  you, so the amount credited to your wallet is **lower** than this value.

So use `actual_paid_amount` to check what the buyer paid against what you
charged. Do **not** use it as the figure to reconcile your wallet balance
against — use your own on-chain record of the settlement transfer for that.

`amount_cents` and `amount_coins` remain the amount you *requested*, unchanged
by what was actually paid; comparing `actual_paid_amount` against them is how you
detect an underpayment or overpayment.

## Actual Webhook Body Example (Matches Real Structure)

### Fiat-priced intent (merchant created with `currency`)

```json theme={null}
{
  "all_scale_transaction_id": "txn_123",
  "all_scale_checkout_intent_id": "chk_456",
  "webhook_id": "whk_84f12a8d",

  "amount_cents": 1234,
  "currency": 1,
  "currency_symbol": "USD",

  "amount_coins": "12.340000",
  "coin_contract_address": "0xdAC17F958D2ee523a2206206994597C13D831ec7",
  "coin_symbol": "USDT",

  "chain_id": 1,
  "tx_hash": "0x...",
  "tx_from": "0x...",

  "payment_method_type": 1,
  "user_id": "user_001",
  "order_id": "order_8899",
  "user_name": "Alice",
  "extra_obj": {
    "source": "mobile",
    "note": "promo-applied"
  },

  "status": 20,
  "actual_paid_amount": "12.340000"
}
```

### Stable-coin-priced intent (merchant created with `stable_coin`)

`amount_cents` is still an integer and equals `amount_coins * 100`.
`currency` and `currency_symbol` are `null` — the buyer paid the merchant
directly in `coin_symbol`, no fiat currency is involved.

```json theme={null}
{
  "all_scale_transaction_id": "txn_123",
  "all_scale_checkout_intent_id": "chk_456",
  "webhook_id": "whk_84f12a8d",

  "amount_cents": 1000,
  "currency": null,
  "currency_symbol": null,

  "amount_coins": "10.000000",
  "coin_contract_address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
  "coin_symbol": "USDC",

  "chain_id": 1,
  "tx_hash": "0x...",
  "tx_from": "0x...",

  "payment_method_type": 1,
  "user_id": "user_001",
  "order_id": "order_8899",
  "user_name": "Alice",
  "extra_obj": null,

  "status": 20,
  "actual_paid_amount": "10.000000"
}
```

***

## Verification Flow (Your Server Side)

1. Extract headers
2. Validate timestamp (±300 seconds)
3. Validate nonce (store with TTL)
4. Read raw request body bytes (before parsing JSON)
5. Compute SHA256 of raw body
6. Rebuild canonical string
7. Compute expected signature
8. Timing-safe compare
9. Only after verification → process payload

***

## Response Expectations

Your endpoint should respond:

* `200 OK` if processed successfully
* Non-200 if rejected (signature invalid, timestamp invalid, etc.)

AllScale will log the HTTP status and may retry depending on configured
policy.

***

## Best Practices

✅ Always verify `X-Webhook-Signature` before processing\
✅ Validate timestamp within ±5 minutes\
✅ Cache nonce for replay protection\
✅ Use idempotency via `webhook_id`\
✅ Convert `amount_coins` using Decimal (not float)\
❌ Never log secrets or signatures

***

## Troubleshooting

| Issue              | Cause            | Fix                                         |
| ------------------ | ---------------- | ------------------------------------------- |
| Signature mismatch | Body modified    | Use raw bytes exactly as received           |
| Signature mismatch | Wrong path/query | Use the exact request path and query string |
| Signature mismatch | Wrong secret     | Verify correct `api_secret`                 |
| Timestamp rejected | Clock drift      | Sync server time (NTP)                      |
| Replay rejected    | Nonce reused     | Generate unique nonce; store with TTL       |

***

End of Document
