Skip to main content
Questions or issues? Contact us at api-support@manus.ai. Every webhook request from Manus includes a cryptographic signature. Verifying it ensures the request is authentic and hasn’t been tampered with.

How it works

Manus signs each request using RSA-SHA256 (2048-bit key). Your server verifies the signature with our public key.
1

Extract headers

Read X-Webhook-Signature and X-Webhook-Timestamp from the incoming request.
2

Check timestamp

Reject requests older than 5 minutes to prevent replay attacks.
3

Reconstruct the signed content

Concatenate: {timestamp}.{url}.{sha256_hex(body)}
4

Verify signature

Hash the content with SHA-256, then verify the signature using the public key.

Request headers

HeaderDescription
X-Webhook-SignatureBase64-encoded RSA-SHA256 signature
X-Webhook-TimestampUnix timestamp (seconds) when the request was signed

Signature format

The signature is computed over this string:
{timestamp}.{url}.{body_sha256_hex}
ComponentDescription
timestampValue of X-Webhook-Timestamp
urlFull webhook URL including query parameters
body_sha256_hexSHA-256 hash of the raw request body, hex-encoded
Example:
1704067200.https://api.yourapp.com/webhooks.a1b2c3d4e5f6...

Get the public key

Fetch the public key from the webhook.publicKey endpoint:
curl 'https://api.manus.ai/v2/webhook.publicKey' \
  -H 'x-manus-api-key: YOUR_API_KEY'
{
  "ok": true,
  "public_key": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQE...\n-----END PUBLIC KEY-----",
  "algorithm": "RSA-SHA256"
}
Cache this key on your server with a reasonable TTL (e.g. 1 hour). Do not fetch it on every webhook request.

Verification examples

import base64, hashlib, time
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.exceptions import InvalidSignature

def verify_webhook(public_key_pem, url, body, signature_b64, timestamp):
    # 1. Check timestamp freshness (5-minute window)
    if abs(int(time.time()) - int(timestamp)) > 300:
        return False

    # 2. Reconstruct signed content
    body_hash = hashlib.sha256(body).hexdigest()
    signed_content = f"{timestamp}.{url}.{body_hash}".encode()

    # 3. Hash the content
    content_hash = hashlib.sha256(signed_content).digest()

    # 4. Verify signature
    try:
        key = serialization.load_pem_public_key(public_key_pem.encode())
        key.verify(
            base64.b64decode(signature_b64),
            content_hash,
            padding.PKCS1v15(),
            hashes.SHA256()
        )
        return True
    except (InvalidSignature, Exception):
        return False
Flask handler:
@app.route("/webhooks", methods=["POST"])
def handle_webhook():
    sig = request.headers.get("X-Webhook-Signature")
    ts = request.headers.get("X-Webhook-Timestamp")

    if not sig or not ts:
        return "Missing headers", 400

    if not verify_webhook(get_cached_public_key(), request.url, request.data, sig, ts):
        return "Unauthorized", 401

    # Process webhook
    data = request.json
    process_event(data)
    return "OK", 200

Best practices

PracticeWhy
Cache the public keyAvoid fetching on every request — it rarely changes
Verify timestampsReject requests older than 5 minutes to prevent replay attacks
Use HTTPSEnsure your webhook endpoint uses TLS
Return generic errorsDon’t expose internal details in 401 responses