UGCDocumentation

Webhook Payload

Webhook event structure and signature verification

Updated July 14, 2026

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:

json
{
  "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

FieldTypeDescription
idstringUnique event ID (format: evt_[hex]) - matches the X-UGC-Event-Id header
triggerstringThe trigger name that fired (see )
timestampstringEvent timestamp (ISO 8601)
dataobjectTrigger-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:

TriggerTriggered When
campaign_createdA new campaign is created
campaign_fundedA campaign is funded (checkout, wallet, or payment link)
campaign_activatedA funded campaign moves to ACTIVE
slot_reservedA creator reserves a slot on a campaign
slot_submittedA creator submits proof for a reserved slot
slot_approvedA submitted slot is approved and earnings accrue
earnings_creditedAn approval credits earnings - data.payoutPercent/data.flatPayoutTierApplied tell you whether a FLAT campaign's performance tiers reduced the credit
earnings_releasedPending earnings clear into the creator wallet
withdrawal_initiatedCash-out accepted (bank, mobile money, or crypto)
withdrawal_reviewCash-out held for review - see data.reviewReason
withdrawal_completedCash-out settled on the rail
withdrawal_failedCash-out failed - see data.failureReason

HTTP Headers

Every webhook delivery includes these headers:

HeaderDescriptionExample
X-UGC-SignatureHex HMAC-SHA256 signature of the raw JSON body, keyed with your hookSecret3f2a9c...
X-UGC-Event-IdUnique delivery ID - use for idempotency/dedupeevt_1709123456_abc123
Content-TypeAlways application/jsonapplication/json
User-AgentUGC automation delivery agentUGC-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)

Example: Node.js
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:

bash
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?