# Webhooks

To simulate and verify webhook integration, use the following test endpoints. These endpoints allow you to validate your webhook handling logic, including signature verification, without waiting for real events. Try them via our API Reference (opens new window).

# General

OriginStamp supports two types of webhooks, which you can configure via your API Key Settings. Webhooks are a critical part of your integration, notifying your systems when timestamping or proof-related events occur.

# Proof Webhook

A proof webhook is sent after you request a Proof. This webhook delivers the proof data for your previously submitted hash.

  • A proof webhook URL must be set in the API key settings.
  • The webhook is triggered shortly (typically < 1 minute) after proof generation.
  • The payload contains all the necessary Merkle data to verify the inclusion of your hash in the anchored tree.

# Tree Webhook

You will receive a tree webhook when the root hash of a Merkle Tree has been successfully anchored on the blockchain. This is essential for the Timestamp Workflow.

  • A tree webhook URL must be set in your API key settings.
  • Once anchored, all hashes submitted during the last aggregation period and included in that tree are considered verified.
  • The webhook payload includes metadata about the tree and blockchain anchoring.

# Delivery & Retry Logic

Webhooks are delivered via POST requests to the configured URLs. Make sure your server is publicly accessible and capable of handling incoming POST requests. If your endpoint fails to respond with a 200 OK, we will retry up to 3 times.

  • Each attempt has a 10-second timeout.
  • Always return a 200 OK response as soon as you have accepted the request.

# Signature Verification

Every webhook request includes an x-signature header containing an HMAC-SHA256 signature of the request body. You can verify this signature using your API key to ensure payload integrity and authenticity.

The signature is generated by:

HMAC_SHA256(secret, canonical_json_body)

Canonical JSON

To ensure consistent signing across languages, the JSON body must:

  1. Have all keys sorted lexicographically.
  2. Have no added whitespace (i.e. compact form).

Example: {"b": 1, "a": 2} must be aligned to {"a":1,"b":2}

# Python

import json
import hmac
import hashlib


def calculate_signature(payload: dict, secret: str) -> str:
    # Sort keys and remove whitespace to ensure deterministic string.
    payload_str = json.dumps(payload, separators=(',', ':'), sort_keys=True)

    signature = hmac.new(
        key=secret.encode("utf-8"),
        msg=payload_str.encode("utf-8"),
        digestmod=hashlib.sha256
    ).hexdigest()

    return signature

# Java

public class WebhookSigner {
    public static String calculateSignature(ObjectNode payload, String secret) throws Exception {
        // Use Jackson to serialize with sorted keys and no pretty print
        ObjectMapper mapper = new ObjectMapper();
        mapper.configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true);

        // Canonical JSON: sorted keys and compact format
        String jsonString = mapper.writeValueAsString(payload);

        Mac hmacSha256 = Mac.getInstance("HmacSHA256");
        SecretKeySpec keySpec = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
        hmacSha256.init(keySpec);

        byte[] hmacBytes = hmacSha256.doFinal(jsonString.getBytes(StandardCharsets.UTF_8));
        StringBuilder hexString = new StringBuilder();
        for (byte b : hmacBytes) {
            hexString.append(String.format("%02x", b));
        }
        return hexString.toString();
    }
}

# TypeScript / Node.js

import * as crypto from 'crypto';

function canonicalizeJson(obj: any): string {
    if (obj === null || typeof obj !== 'object') return JSON.stringify(obj);
    if (Array.isArray(obj)) return `[${obj.map(canonicalizeJson).join(',')}]`;
    
    const sortedKeys = Object.keys(obj).sort();
    const entries = sortedKeys.map(key => `"${key}":${canonicalizeJson(obj[key])}`);
    return `{${entries.join(',')}}`;
}

function calculateSignature(payload: any, secret: string): string {
    const canonicalJson = canonicalizeJson(payload);
    
    return crypto
    .createHmac('sha256', secret)
    .update(canonicalJson, 'utf8')
    .digest('hex');
}

# Example

Given the API secret: non-valid-api-key

With the canonical JSON for the following payload:

{
  "treeId": "3f9474cd-a8b1-418e-bcad-88233049fe92",
  "dateCreated": 1754328093419,
  "currencyId": 0,
  "currency": "BTC",
  "submitStatus": 3,
  "timestamp": 1710000000000,
  "rootSha256": "3fd4bc2b4f14b9798c23a50d09b61741f8594b8bb2bb4842f5e2b25797c06dbc",
  "transaction": "0x3f289856c20c0471fb335db48d4df28718ff2c005b3cf7f80231ba52649b853f"
}

The resulting signature would be:

188f5a41b0d3f011b038dca26f6ca6ef3b3e1a886337f8683601017a6b531625

# Testing

# Tree Verification Test

This simulates a Merkle Tree verification event. The payload is static and does not change. It can be used to test your webhook handling logic for tree verification events.

POST /v1/webhook/tree

Request:

{
  "target": "https://yourdomain.com/webhooks/tree"
}

# Proof Delivery Test

This simulates a complete timestamp proof delivery, including Merkle branch data. The payload is static and does not change. It can be used to test your webhook handling logic for tree verification events.

POST /v1/webhook/proof

Request:

{
  "target": "https://yourdomain.com/webhooks/proof"
}

# Security Notes

  • An x-api-key header is also required for all test requests.
  • Use HTTPS for all webhook targets to protect against MITM attacks.
  • All webhook requests are signed using HMAC-SHA256 via the x-signature header.
  • Webhook targets should verify the signature before trusting the payload.

Abuse of this testing service (e.g., targeting third-party services) may result in it being rate-limited or disabled without prior notice.