Webhooks allow your application to receive real-time, push-based HTTP notifications when smart contract events occur on the Stellar network. Instead of polling the SoroScan API, your server can immediately process events like token transfers, swaps, or custom contract states.
You can create a webhook subscription by making a POST request to the SoroScan REST API.
POST /api/webhooks/
target_url(String, Required): The public URL of your server's endpoint where SoroScan will send HTTP POST requests.contract_id(String, Required): The Soroban contract address you want to monitor (e.g.,CCAAA...).event_types(Array of Strings, Optional): List of specific event symbols to filter for (e.g.,["transfer", "mint"]). If omitted, all events for the contract are delivered.filter_condition(String, Optional): A filter expression to evaluate against event fields.
curl -X POST https://api.soroscan.io/api/webhooks/ \
-H "Authorization: Token your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"target_url": "https://your-domain.com/webhooks/soroscan",
"contract_id": "CCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
"event_types": ["transfer"]
}'{
"id": 42,
"contract_id": "CCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
"target_url": "https://your-domain.com/webhooks/soroscan",
"event_types": ["transfer"],
"status": "active",
"is_active": true,
"created_at": "2026-07-28T14:15:00Z"
}To verify that your webhook receiver is working and reachable, you can trigger a test delivery using the SoroScan API:
curl -X POST https://api.soroscan.io/api/webhooks/42/ping/ \
-H "Authorization: Token your_api_key_here"This will queue a background task in Celery to send a mock ping payload to your target_url.
To ensure webhook events originate from SoroScan and have not been spoofed, SoroScan signs every webhook payload using Ed25519. The signature is sent in the X-Signature header in the following format:
X-Signature: ed25519=SIGNATURE_IN_BASE64You can obtain the platform's public key from the API metadata endpoint or your developer portal.
Below is a copy-pasteable example of an Express receiver verifying the signature using the native crypto module (no external library required):
const express = require('express');
const crypto = require('crypto');
const app = express();
// SoroScan Platform Public Key (obtain from developer portal)
const PLATFORM_PUBLIC_KEY_B64 = "YOUR_PLATFORM_PUBLIC_KEY_BASE64";
// We need raw request body bytes to verify the signature accurately
app.use(express.raw({ type: 'application/json' }));
app.post('/webhooks/soroscan', (req, res) => {
const signatureHeader = req.headers['x-signature'];
if (!signatureHeader || !signatureHeader.startsWith('ed25519=')) {
console.error('Missing or invalid X-Signature header');
return res.status(401).send('Unauthorized');
}
// Extract signature from header prefix "ed25519="
const signatureB64 = signatureHeader.substring(8);
const signature = Buffer.from(signatureB64, 'base64');
const publicKeyBytes = Buffer.from(PLATFORM_PUBLIC_KEY_B64, 'base64');
try {
const publicKey = crypto.createPublicKey({
key: publicKeyBytes,
format: 'raw',
type: 'ed25519',
});
const isVerified = crypto.verify(null, req.body, publicKey, signature);
if (!isVerified) {
console.warn('Signature verification failed');
return res.status(401).send('Invalid signature');
}
// Process the verified payload
const event = JSON.parse(req.body.toString());
console.log('Verified event received:', event);
res.status(200).send('Event processed');
} catch (error) {
console.error('Error verifying signature:', error);
res.status(500).send('Internal Server Error');
}
});
app.listen(3000, () => console.log('Webhook server listening on port 3000'));To verify webhook signatures in Python, use the standard cryptography library:
import base64
from flask import Flask, request, jsonify
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
app = Flask(__name__)
PLATFORM_PUBLIC_KEY_B64 = "YOUR_PLATFORM_PUBLIC_KEY_BASE64"
def verify_signature(public_key_b64: str, signature_header: str, payload_bytes: bytes) -> bool:
if not signature_header or not signature_header.startswith("ed25519="):
return False
try:
signature_b64 = signature_header[8:]
signature = base64.b64decode(signature_b64)
public_key_bytes = base64.b64decode(public_key_b64)
public_key = Ed25519PublicKey.from_public_bytes(public_key_bytes)
public_key.verify(signature, payload_bytes)
return True
except Exception:
return False
@app.route('/webhooks/soroscan', methods=['POST'])
def handle_webhook():
signature_header = request.headers.get("X-Signature", "")
payload_bytes = request.data # Get raw body bytes
if not verify_signature(PLATFORM_PUBLIC_KEY_B64, signature_header, payload_bytes):
return "Invalid signature", 401
event_data = request.get_json()
print(f"Verified event: {event_data}")
return "Success", 200
if __name__ == '__main__':
app.run(port=5000)If your webhook target is not receiving events as expected, consult the following steps.
You can query the history of webhook attempts directly from SoroScan:
- Endpoint:
GET /api/webhooks/{id}/deliveries/ - This endpoint returns the status of the last 30 days of deliveries, including the HTTP status code returned by your server, response headers, and connection errors.
- Timeout Limits: Your endpoint must respond to a webhook POST request within 5 seconds. If your processing takes longer, process the event asynchronously (e.g., using a message queue) and immediately return
200 OKto SoroScan. - Success Codes: Only responses in the
2xxrange (e.g.,200 OK,202 Accepted) are marked as successful. Any other status code (like500or404) is classified as a delivery failure.
SoroScan uses an exponential backoff policy for failed webhook deliveries:
- Failed attempts are retried up to 5 times.
- If a webhook subscription experiences 5 consecutive failed attempts (either due to timeouts or non-2xx status codes), SoroScan automatically suspends the webhook to conserve system resources.
Once a webhook has been suspended:
- Fix the underlying issue on your endpoint server.
- Reactivate the webhook by making a
PUTorPATCHrequest to the subscription details:curl -X PATCH https://api.soroscan.io/api/webhooks/42/ \ -H "Authorization: Token your_api_key_here" \ -H "Content-Type: application/json" \ -d '{"status": "active"}'
- You can also manually trigger retries of failed deliveries in the administrative dashboard's dead-letter queue.
You can exercise a receiver without running Django, Celery, Redis, or PostgreSQL. The standalone simulator in tools/webhook-simulator/ POSTs the same JSON envelope and HMAC headers as production dispatch_webhook.
cd tools/webhook-simulator
pip install -e .
python examples/receiver.py
# in another terminal
webhook-simulator --url http://127.0.0.1:8080/webhook --sample --secret test-secretDocker (hits a listener on the host):
docker compose -f tools/webhook-simulator/docker-compose.yml run --rm webhook-simulator \
--url http://host.docker.internal:8080/webhook --sample --secret test-secretThe CLI prints delivery status, HTTP status, latency, acknowledgement (X-SoroScan-Ack), response headers, and a truncated response body. Use --output json for a machine-readable result, --dry-run to inspect the signed request, and --help for retries and Ed25519 (X-Signature) options. See tools/webhook-simulator/README.md for the full payload contract.