Skip to main content

Webhooks

Paratro sends HTTP POST webhook notifications for transaction lifecycle events — from initial detection on-chain through final confirmation. All webhook requests are authenticated using HMAC-SHA256 signatures.

Event Types

Webhooks are sent at each stage of a transaction’s lifecycle:
Event TypeDescriptionWhen
transaction.confirmingTransaction detected on-chainBlock mined but < required confirmations
transaction.confirmedTransaction fully confirmedRequired confirmations reached
transaction.failedTransaction failed on-chainTransaction reverted
x402.settlement.confirmedx402 settlement completedFacilitator settle tx confirmed
x402.settlement.failedx402 settlement failedFacilitator settle tx reverted

Confirmation Requirements

ChainConfirmationsEstimated Time
Ethereum12~2.5 minutes
BSC15~45 seconds
Polygon12~24 seconds
Base / Optimism / Arbitrum12~24 seconds
Tron19~57 seconds
Bitcoin1~10 minutes
Solana0 (finalized)Instant
Solana uses finalized commitment — transactions are confirmed immediately. You will only receive transaction.confirmed, not transaction.confirming.

Trigger Conditions

Webhook notifications are sent when all of the following are met:
  1. Wallet account for the address (to or from) exists in the system
  2. Client status is ACTIVE
  3. Configuration — both WebhookUrl and WebhookSecret are configured

Request Format

Method: POST Headers:
HeaderDescription
Content-Typeapplication/json
X-Paratro-TimestampUnix timestamp (seconds)
X-Paratro-Signaturev1= + hex-encoded HMAC-SHA256 signature
X-Paratro-Api-KeyClient API Key
Body:
FieldTypeDescription
event_idstringUnique UUID for this webhook event
event_typestringEvent type (see Event Types table)
event_timestringISO 8601 timestamp when webhook was sent
source_idstringSource identifier — transaction ID (tx_id) for transaction events, settlement ID for x402 events
wallet_idstringWallet UUID (empty for x402 settlement events)
account_idstringAccount UUID (empty for x402 settlement events)
statusstringCONFIRMING, CONFIRMED, FAILED, or SETTLED
transaction_typestringINBOUND or OUTBOUND
chainstringBlockchain (ethereum, bsc, polygon, base, tron, solana, bitcoin)
networkstringNetwork environment: testnet or mainnet
txhashstringOn-chain transaction hash
block_numbernumberBlock height
fromstringSender address
tostringRecipient address
symbolstringToken symbol (e.g., ETH, USDC)
contract_addressstringToken contract address (empty for native tokens)
amountstringAmount in smallest unit (e.g., "20000000" for 20 USDC)
decimalsnumberToken decimal places
confirmationsnumberCurrent confirmation count
required_confirmationsnumberRequired confirmations for this chain
created_atstringISO 8601 transaction creation time
confirmed_atstringISO 8601 confirmation time (empty string if not yet confirmed)
risk_checkedbooleanWhether compliance scanning was performed
risk_scorenumberRisk score (0 if not scanned)
risk_levelstringLOW, MEDIUM, HIGH, or UNSCANNED
datastringHex-encoded EVM transaction calldata. Only populated for EVM chains (Ethereum, BSC, Polygon, Base). Always empty for Solana, Bitcoin, and Tron (these chains have different data models — use txhash to fetch full tx details from chain RPC if needed).

Example Payloads

{
  "event_id": "6c2c7d32-8e89-46b1-a091-d2df94d12937",
  "event_type": "transaction.confirming",
  "event_time": "2026-04-16T10:23:45Z",
  "source_id": "a39acd1c-7339-4655-9e30-d7955264d39d",
  "wallet_id": "982af2fe-b47c-4a28-9ddc-1dcaa4e5a128",
  "account_id": "e1f0bf23-c42c-443a-b268-c615bf1d4339",
  "status": "CONFIRMING",
  "transaction_type": "INBOUND",
  "chain": "ethereum",
  "network": "testnet",
  "txhash": "0x5428546a431be89034297e09aab760461481b3e6dec16c856413a4f8a67d2e07",
  "block_number": 10668809,
  "from": "0x319dd63e0ac72e7ac74443029d074032c043460f",
  "to": "0xc901013a8731a5af5b5dca336fa9a13a1d22b354",
  "symbol": "USDC",
  "contract_address": "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238",
  "amount": "20000000",
  "decimals": 6,
  "confirmations": 0,
  "required_confirmations": 6,
  "created_at": "2026-04-16T10:23:45Z",
  "confirmed_at": "",
  "risk_checked": false,
  "risk_score": 0,
  "risk_level": "UNSCANNED",
  "data": ""
}

Transaction Lifecycle

On-chain detection → CONFIRMING webhook → ... waiting for confirmations ... → CONFIRMED webhook → Balance credited
1

Transaction Detected

The system detects the transaction in a newly mined block. A transaction.confirming webhook is sent immediately — the transaction is now visible in your dashboard.
2

Awaiting Confirmations

The system monitors the blockchain until the required number of confirmations is reached. No additional webhooks during this phase.
3

Transaction Confirmed

Once confirmed, a transaction.confirmed webhook is sent. For inbound deposits, the amount is credited to the account balance.

Signature Verification

All webhook requests include an HMAC-SHA256 signature following the Stripe-style signing scheme.

Algorithm

  1. Build canonical string: {timestamp}.{raw_request_body}
  2. Compute: "v1=" + hex(HMAC-SHA256(webhook_secret, canonical_string))
  3. Compare with X-Paratro-Signature header using constant-time comparison

Go Verification Code

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "fmt"
    "strconv"
    "time"
)

const DefaultTolerance = 5 * time.Minute

func VerifyPayload(secret, timestamp string, payload []byte, signature string, tolerance time.Duration) error {
    ts, err := strconv.ParseInt(timestamp, 10, 64)
    if err != nil {
        return fmt.Errorf("webhook: invalid timestamp: %w", err)
    }

    if tolerance > 0 {
        diff := time.Since(time.Unix(ts, 0))
        if diff < 0 {
            diff = -diff
        }
        if diff > tolerance {
            return fmt.Errorf("webhook: timestamp too old (age: %v)", diff)
        }
    }

    canonical := make([]byte, 0, len(timestamp)+1+len(payload))
    canonical = append(canonical, timestamp...)
    canonical = append(canonical, '.')
    canonical = append(canonical, payload...)

    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write(canonical)
    expected := "v1=" + hex.EncodeToString(mac.Sum(nil))

    if !hmac.Equal([]byte(expected), []byte(signature)) {
        return fmt.Errorf("webhook: signature mismatch")
    }
    return nil
}

Response Requirements

Return HTTP 200 to acknowledge receipt:
{ "success": true, "message": "Webhook received successfully" }
Response timeout: 10 seconds. If no response within 10 seconds, the request is considered failed.

Retry Policy

Failed deliveries are retried with exponential backoff:
AttemptDelay
1Immediate
225 seconds
3125 seconds (~2 min)
4625 seconds (~10 min)

Best Practices

  • Idempotency — Use source_id + event_type as a unique identifier to prevent duplicate processing
  • Handle both events — Process transaction.confirming for UI updates, transaction.confirmed for balance crediting
  • Async processing — Return 200 immediately, process in the background
  • HTTPS — Always use HTTPS for your webhook URL
  • Secret protection — Store WebhookSecret securely, never in source code
  • Signature verification — Always verify signatures before processing
  • Timestamp validation — Validate timestamps to prevent replay attacks (default: 5 minute window)

Security Guarantees

ThreatMitigation
Body tamperingHMAC-SHA256 integrity verification
Signature forgeryRequires webhook secret
Replay attackTimestamp window validation
Timing attackConstant-time comparison (hmac.Equal)