Real-time events, signed and reliable.
Subscribe to outgoing webhooks to react to campaign activity, submissions, perk redemptions, and fraud signals as they happen.
Header: X-SocialPerks-Signature
Up to 8 attempts over 24 hours on non-2xx responses.
Respond fast, queue work async. 200/204 = success.
Events
Pick the events you want and we'll POST them to your endpoint.
campaign.createdPOSTFires when a new campaign is created in any account.
{
"id": "evt_01HK3...",
"type": "campaign.created",
"created": 1715300000,
"data": {
"campaignId": "cmp_01HK3...",
"businessId": "biz_yoga",
"name": "Spring Yoga Push",
"tier": "high-impact",
"status": "draft"
}
}submission.receivedPOSTFires when a customer submits proof of action.
{
"id": "evt_01HK3...",
"type": "submission.received",
"created": 1715300100,
"data": {
"submissionId": "sub_01HK3...",
"campaignId": "cmp_01HK3...",
"userId": "usr_01HK3...",
"platform": "instagram",
"proofUrl": "https://instagram.com/p/CxYz..."
}
}submission.approvedPOSTFires after a submission passes review.
{
"id": "evt_01HK3...",
"type": "submission.approved",
"created": 1715300200,
"data": {
"submissionId": "sub_01HK3...",
"approvedBy": "auto-review",
"score": 0.94,
"rewardCents": 1000
}
}submission.rejectedPOSTFires when a submission is rejected by reviewer or AI.
{
"id": "evt_01HK3...",
"type": "submission.rejected",
"created": 1715300210,
"data": {
"submissionId": "sub_01HK3...",
"reason": "missing_ftc_disclosure",
"reviewer": "auto-review"
}
}perk.earnedPOSTFires when a customer earns a perk reward.
{
"id": "evt_01HK3...",
"type": "perk.earned",
"created": 1715300300,
"data": {
"perkId": "prk_01HK3...",
"userId": "usr_01HK3...",
"valueCents": 1000,
"currency": "USD",
"expiresAt": 1717892300
}
}perk.redeemedPOSTFires when a customer redeems an earned perk.
{
"id": "evt_01HK3...",
"type": "perk.redeemed",
"created": 1715300400,
"data": {
"perkId": "prk_01HK3...",
"userId": "usr_01HK3...",
"redeemedAt": 1715300400,
"location": "store_01"
}
}perk.expiredPOSTFires when an earned perk expires unredeemed.
{
"id": "evt_01HK3...",
"type": "perk.expired",
"created": 1717892300,
"data": {
"perkId": "prk_01HK3...",
"userId": "usr_01HK3...",
"expiredAt": 1717892300
}
}influencer.matchedPOSTFires when the matching engine pairs an influencer with a campaign.
{
"id": "evt_01HK3...",
"type": "influencer.matched",
"created": 1715300500,
"data": {
"campaignId": "cmp_01HK3...",
"influencerId": "inf_01HK3...",
"score": 0.87
}
}fraud.flaggedPOSTFires when the fraud engine flags suspicious activity.
{
"id": "evt_01HK3...",
"type": "fraud.flagged",
"created": 1715300600,
"data": {
"submissionId": "sub_01HK3...",
"signals": [
"duplicate_image",
"low_account_age"
],
"severity": "high"
}
}campaign.launchedPOSTFires when a campaign goes live.
{
"id": "evt_01HK3...",
"type": "campaign.launched",
"created": 1715300700,
"data": {
"campaignId": "cmp_01HK3...",
"launchedAt": 1715300700
}
}Receive and verify
Always verify the signature before trusting a payload.
import crypto from "crypto";
import express from "express";
const app = express();
const SECRET = process.env.SOCIAL_PERKS_WEBHOOK_SECRET;
app.post("/webhooks/social-perks", express.raw({ type: "application/json" }), (req, res) => {
const signature = req.header("X-SocialPerks-Signature");
const expected = crypto
.createHmac("sha256", SECRET)
.update(req.body)
.digest("hex");
if (signature !== expected) return res.status(401).send("invalid signature");
const event = JSON.parse(req.body.toString());
console.log("event:", event.type, event.data);
res.status(200).send("ok");
});
app.listen(3000);import hmac
import hashlib
import os
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["SOCIAL_PERKS_WEBHOOK_SECRET"].encode()
@app.post("/webhooks/social-perks")
def receive():
signature = request.headers.get("X-SocialPerks-Signature", "")
expected = hmac.new(SECRET, request.data, hashlib.sha256).hexdigest()
if not hmac.compare_digest(signature, expected):
abort(401)
event = request.get_json()
print("event:", event["type"], event["data"])
return "ok", 200<?php
$secret = getenv("SOCIAL_PERKS_WEBHOOK_SECRET");
$payload = file_get_contents("php://input");
$signature = $_SERVER["HTTP_X_SOCIALPERKS_SIGNATURE"] ?? "";
$expected = hash_hmac("sha256", $payload, $secret);
if (!hash_equals($expected, $signature)) {
http_response_code(401);
exit("invalid signature");
}
$event = json_decode($payload, true);
error_log("event: " . $event["type"]);
http_response_code(200);
echo "ok";require "sinatra"
require "openssl"
require "json"
SECRET = ENV.fetch("SOCIAL_PERKS_WEBHOOK_SECRET")
post "/webhooks/social-perks" do
payload = request.body.read
signature = request.env["HTTP_X_SOCIALPERKS_SIGNATURE"]
expected = OpenSSL::HMAC.hexdigest("SHA256", SECRET, payload)
halt 401, "invalid signature" unless Rack::Utils.secure_compare(expected, signature)
event = JSON.parse(payload)
logger.info "event: #{event['type']}"
status 200
"ok"
endSignature verification
- 1. Read the raw body. Do not parse JSON before computing the HMAC — even whitespace changes break the signature.
- 2. Compute HMAC-SHA256. Use your webhook secret (from dashboard → Developers → Webhooks) as the key, the raw body as the message.
- 3. Constant-time compare. Use a timing-safe comparator (
hmac.compare_digest,crypto.timingSafeEqual) to avoid leaking the signature. - 4. Check freshness. Reject events older than 5 minutes (
X-SocialPerks-Timestamp) to prevent replay attacks.
Retry & timeout policy
- Success. Any 2xx response within 5 seconds counts as delivered.
- Failure. Non-2xx, timeout, or connection error triggers a retry.
- Backoff. Up to 8 attempts spaced exponentially over 24 hours: 1m, 5m, 15m, 1h, 3h, 6h, 12h, 24h.
- Idempotency. Each delivery has the same
id. Use it as your dedup key — retries are expected. - Dead-letter. After the final failure, events appear in your dashboard's webhook log for manual replay.
Start receiving events
Configure your endpoint in the dashboard and we'll send a test event to confirm.