Webhook Payload
Webhook event structure and signature verification
Webhook payloads are sent to your subscribed target URL when a trigger you're subscribed to fires.
Payload Structure
Every delivery follows this consistent envelope:
{
"id": "evt_3f9a1c2b8e7d4f6a",
"trigger": "campaign_funded",
"timestamp": "2026-08-26T10:15:00Z",
"data": {
"campaignId": "cmp_a3k9x2pqw1",
"amount": 50000,
"currency": "NGN",
"fundedVia": "payment_link",
"integrationProvider": "AUTOMATION"
}
}Fields
Envelope Fields
| Field | Type | Description |
|---|---|---|
| id | string | Unique event ID (format: evt_[hex]) - matches the X-UGC-Event-Id header |
| trigger | string | The trigger name that fired (see ) |
| timestamp | string | Event timestamp (ISO 8601) |
| data | object | Trigger-specific payload - shape varies by trigger, always includes integrationProvider: "AUTOMATION" |
There is no top-level event/livemode field - every key is live from creation (see ), so there is no test-mode flag to check.
Event Types (Triggers)
Webhooks are configured as Automation trigger subscriptions, not a fixed payment-events list. The subscribe API accepts 31 trigger names (AUTOMATION_TRIGGER_LIST) covering campaign/slot/earnings/withdrawal/AI/storage lifecycle plus kept merchant-billing rails (payment intents, subscriptions, usernames). See the for exact names and payload shapes - a few common ones:
| Trigger | Triggered When |
|---|---|
campaign_created | A new campaign is created |
campaign_funded | A campaign is funded (checkout, wallet, or payment link) |
campaign_activated | A funded campaign moves to ACTIVE |
slot_reserved | A creator reserves a slot on a campaign |
slot_submitted | A creator submits proof for a reserved slot |
slot_approved | A submitted slot is approved and earnings accrue |
earnings_credited | An approval credits earnings - data.payoutPercent/data.flatPayoutTierApplied tell you whether a FLAT campaign's performance tiers reduced the credit |
earnings_released | Pending earnings clear into the creator wallet |
withdrawal_initiated | Cash-out accepted (bank, mobile money, or crypto) |
withdrawal_review | Cash-out held for review - see data.reviewReason |
withdrawal_completed | Cash-out settled on the rail |
withdrawal_failed | Cash-out failed - see data.failureReason |
HTTP Headers
Every webhook delivery includes these headers:
| Header | Description | Example |
|---|---|---|
X-UGC-Signature | Hex HMAC-SHA256 signature of the raw JSON body, keyed with your hookSecret | 3f2a9c... |
X-UGC-Event-Id | Unique delivery ID - use for idempotency/dedupe | evt_1709123456_abc123 |
Content-Type | Always application/json | application/json |
User-Agent | UGC automation delivery agent | UGC-Automation-Bot/1.0 |
There is no timestamp header and no sha256= prefix on the signature - it is the raw hex digest.
Signature Verification
Always verify `X-UGC-Signature` to ensure requests are from UGC.
Verification Steps
1. Extract the X-UGC-Signature header
2. Compute HMAC-SHA256 of the raw request body using your hookSecret (from the subscribe response)
3. Compare the hex digest with the header value using a constant-time comparison
4. Use X-UGC-Event-Id to skip events you've already processed (deliveries can be retried)
const crypto = require('crypto');
function verifySignature(signatureHeader, rawBody, hookSecret) {
const expected = crypto
.createHmac('sha256', hookSecret)
.update(rawBody)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signatureHeader),
Buffer.from(expected)
);
}
// In your webhook handler (mount with a raw-body parser)
app.post('/webhooks/ugc', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-ugc-signature'];
const eventId = req.headers['x-ugc-event-id'];
if (!verifySignature(signature, req.body, process.env.UGC_HOOK_SECRET)) {
return res.status(401).send('Unauthorized');
}
const event = JSON.parse(req.body);
switch (event.trigger) {
case 'campaign_funded':
// Handle campaign_funded
break;
case 'slot_approved':
// Handle slot_approved
break;
// ... other triggers
}
res.status(200).send('OK');
});Hook Secret Format
Your hook secret has the format: whsec_xxx and is returned once, in the response to POST /automation/webhooks/subscribe.
Retry Logic
UGC automatically retries failed webhook deliveries:
- Total Attempts: Up to 5 attempts
- Failure Condition: Non-2xx HTTP response or timeout (30 seconds)
Testing Webhooks
Subscribe to a trigger and trigger the underlying action (e.g. fund a campaign) to see a real delivery:
curl -X POST https://api.ugc.africa/automation/webhooks/subscribe \
-H "Authorization: Bearer UGSK_live_abc123..." \
-H "Content-Type: application/json" \
-d '{"trigger": "campaign_funded", "targetUrl": "https://yourapp.com/webhooks/ugc"}'Best Practices
1. Always verify signatures - Never skip signature verification, even in development
2. Return 200 quickly - Process webhooks asynchronously, return 200 within 5 seconds
3. Handle idempotency - Use X-UGC-Event-Id to prevent processing duplicate deliveries
4. Log everything - Log all webhook deliveries for debugging and audit trails
5. Monitor failures - Set up alerts for webhook delivery failures
Did this solve your problem?