Webhooks
1. What & why
Section titled “1. What & why”FSCO can push events to your backend instead of making you poll our API. You can subscribe to specific events and we’ll POST
a JSON payload to your server seconds after it happens.
2. Create a webhook
Section titled “2. Create a webhook”- Go to Dashboard → Developers → Webhooks
- Click New Webhook
- Enter your destination URL
- Pick events you want to subscribe to
- Copy your Signing Secret
ℹ️ Webhooks are orginsation-scoped. We recommend using our staging environment to test your webhook before using them in production. Contact us if you need to use a different environment.
3. Payload format
Section titled “3. Payload format”Every webhook sent by FSCO follows a consistent structure, regardless of the originating service:
{ "id": "85a94d40-370a-49a8-b328-383477a4a710", // Unique identifier for the event "type": "listing.created", // The type of event that occurred "created": "2025-05-05T05:33:21.456Z", // Timestamp of when the event occurred "data": { "object": { //Even payload here } }}
We send this as the raw request body with Content-Type: application/json
.
4. Verify the signature
Section titled “4. Verify the signature”Every request has a X-Signature
header:
X-Signature: hex-encoded HMAC-SHA256 // The signature of the event
You can verify the signature using the Signing Secret
you copied when you created the webhook.
Below is a sample implementation in TypeScript / Express.js.
Verification
Section titled “Verification” import * as crypto from 'crypto';
/** * Verifies the FSCO webhook signature. * @param rawBody Raw JSON string (not parsed!) * @param headerSignature Value from "X-Signature" header * @param secret Your webhook's signing secret */ export function verifyFscoSignature( rawBody: string, headerSignature: string, secret: string, ): boolean { const expected = crypto .createHmac('sha256', secret) .update(rawBody) .digest('hex');
try { return crypto.timingSafeEqual( Buffer.from(expected), Buffer.from(headerSignature), ); } catch { return false; // Always return false on mismatch or malformed signature } }
import { verifyFscoSignature } from './verifyFscoSignature';
app.post('/webhook', (req, res) => { const rawBody = (req as any).rawBody; const signature = req.get('X-Signature') || ''; const secret = process.env.FSCO_WEBHOOK_SECRET!;
const isValid = verifyFscoSignature(rawBody, signature, secret);
if (!isValid) { return res.status(400).send('Invalid signature'); }
const event = req.body;
// Process event safely console.log('✅ Valid event received: ${event.type}');
res.status(200).send('OK'); });
5. Retry logic
Section titled “5. Retry logic”If your endpoint is temporarily unreachable or responds with a non-2xx HTTP code, FSCO will automatically retry delivery with exponential backoff.
FSCO retries failed webhook deliveries up to 5 times using exponential backoff starting at 1 second. Each attempt uses an HTTP POST request with Content-Type: application/json
, and every payload is signed using HMAC SHA256
via the X-Signature
header for verification. Requests time out after 10 seconds if no response is received.
We mark each attempt in the dashboard and expose detailed logs including response code, message, and retry count.
What counts as a failure?
Section titled “What counts as a failure?”- No response (timeout or DNS failure)
- Response is not a 2xx status (e.g. 400, 500)
- Response takes longer than 10 seconds
6. Supported events
Section titled “6. Supported events”Below is a list of all supported webhook events you can subscribe to. Each event includes a unique name and a brief description of what it indicates.
Event Name | Description |
---|---|
wallet.created | Wallet created |
webhook.created | Webhook created |
webhook.updated | Webhook updated |
webhook.secret.rolled | Webhook secret rolled |
webhook.tested | Webhook tested |
hcs.topic.created | HCS topic created |
hcs.message.added | HCS message added to topic |
ai.typeSelected | AI Document Type Selected |
ai.processed | AI Prompt Answered |
ai.promptTested | AI Document Prompt Test Processed |
listing.created | Listing created |
listing.state.changed | Listing state changed |
listing.purchase.initiated | Listing purchase initiated |
listing.purchase.progressed | Listing purchase progressed |
listing.purchase.completed | Listing purchase completed |
listing.purchase.failed | Listing purchase failed |
listing.bought | Listing bought |
listing.sold | Listing sold |
marketplace.created | Marketplace created |
asset.minted | Asset minted |
asset.burned | Asset retired |
asset.transferred | Asset transferred |
ocr.started | OCR started processing |
ocr.completed | OCR done processing |
💡 Need a specific event not listed here? Contact support. More are being added all the time!
7. Best practices
Section titled “7. Best practices”- Return a 200 status code (quickly) to acknowledge receipt of the webhook.
- Store the event ID in your database for reference to de-duplicate events.
- Use separate secrets per environment and utilise the staging environment for development before using these in production.
- Do not IP allowlist as this may prevent FSCO from reaching your endpoint. Signature verification is the only way to ensure the request is from FSCO.
Need help? Contact us at support@fsco.io