Skip to main content
Webhooks

Real-time events, signed and reliable.

Subscribe to outgoing webhooks to react to campaign activity, submissions, perk redemptions, and fraud signals as they happen.

Signature
HMAC-SHA256

Header: X-SocialPerks-Signature

Retries
Exponential backoff

Up to 8 attempts over 24 hours on non-2xx responses.

Timeout
5 seconds

Respond fast, queue work async. 200/204 = success.

Events

Pick the events you want and we'll POST them to your endpoint.

campaign.createdPOST

Fires 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.receivedPOST

Fires 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.approvedPOST

Fires 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.rejectedPOST

Fires 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.earnedPOST

Fires 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.redeemedPOST

Fires 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.expiredPOST

Fires when an earned perk expires unredeemed.

{
  "id": "evt_01HK3...",
  "type": "perk.expired",
  "created": 1717892300,
  "data": {
    "perkId": "prk_01HK3...",
    "userId": "usr_01HK3...",
    "expiredAt": 1717892300
  }
}
influencer.matchedPOST

Fires 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.flaggedPOST

Fires 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.launchedPOST

Fires 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.

Node.js
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);
Python
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
<?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";
Ruby
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"
end

Signature verification

  1. 1. Read the raw body. Do not parse JSON before computing the HMAC — even whitespace changes break the signature.
  2. 2. Compute HMAC-SHA256. Use your webhook secret (from dashboard → Developers → Webhooks) as the key, the raw body as the message.
  3. 3. Constant-time compare. Use a timing-safe comparator (hmac.compare_digest, crypto.timingSafeEqual) to avoid leaking the signature.
  4. 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.

Site directory

Sixty deep links into the parts of the site most people miss. Pick a category and start digging.

Industries

Marketing playbooks tailored to your kind of business.

Cities

Local insights for the metros we serve.

Tools

Free calculators and generators.

Guides

Step-by-step playbooks.

Compare

How Social Perks stacks up.

Resources

Everything else worth reading.