UGCDocumentation

Best Practices

Production-ready integration guidelines

Updated July 14, 2026

Follow these best practices to build a secure, reliable, and production-ready UGC integration.

1. Security Best Practices

API Key Management

Never Expose Your API Key

Keep UGSK_xxx keys server-side only. Never include them in:

  • Frontend JavaScript code
  • Mobile app binaries
  • Version control (Git repositories)
  • Client-side configuration files

Use Environment Variables

Bash
# .env file
UGC_API_KEY=UGSK_live_abc123...
WEBHOOK_SECRET=whsec_xyz789...

Rotate Keys Regularly

Generate new API keys periodically (recommended: every 90 days)

Verify Webhook Signatures

Always verify webhook authenticity using HMAC signatures

javascript
function verifyWebhookSignature(rawBody, signatureHeader, hookSecret) {
  const expected = crypto
    .createHmac('sha256', hookSecret)
    .update(rawBody) // the raw request body, not a re-serialized object
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signatureHeader),
    Buffer.from(expected)
  );
}

See for the full verification walkthrough - the signature is a raw hex digest with no sha256= prefix.

Data Protection

Use HTTPS

Always use HTTPS for:

  • Webhook endpoints
  • API calls
  • Redirect URLs

Validate Input

Sanitize and validate all user input before sending to API

Never Log Sensitive Data

Don't log:

  • API keys
  • Webhook secrets
  • Customer payment information
  • Personal identifiable information (PII)

2. API Usage Best Practices

Start Small, Then Scale Up

Every key you issue is a live key from the moment you create it - there is no separate test mode. Validate your integration against a small-budget campaign first, then raise the budget once you're confident it behaves correctly.

javascript
const apiKey = process.env.UGC_API_KEY; // UGSK_live_...

Implement Retry Logic

Retry failed requests with exponential backoff

JavaScript
async function fundCampaignWithRetry(campaignId, data, maxRetries = 3) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      const response = await axios.post(
        `https://api.ugc.africa/automation/actions/campaigns/${campaignId}/fund`,
        data,
        {
          headers: {
            'Authorization': `Bearer ${process.env.UGC_API_KEY}`
          }
        }
      );
      
      return response.data;
    } catch (error) {
      if (error.response?.status === 429) {
        // Rate limited - wait and retry
        const waitTime = Math.pow(2, attempt) * 1000;
        await sleep(waitTime);
        continue;
      }
      
      if (attempt === maxRetries - 1) {
        throw error;
      }
      
      await sleep(Math.pow(2, attempt) * 1000);
    }
  }
}

Handle Errors Gracefully

Provide user-friendly error messages based on the HTTP status, not a machine-readable code - see for the real response shapes.

javascript
try {
  const result = await fundCampaign(campaignId, data);
  return result;
} catch (error) {
  const status = error.response?.status;

  if (status === 401 || status === 403) {
    throw new Error('Authentication failed. Please contact support.');
  }
  if (status === 429) {
    throw new Error('Too many requests. Please try again later.');
  }
  if (status) {
    throw new Error(error.response.data?.message ?? error.response.data?.error ?? 'Request failed.');
  }

  throw new Error('Network error. Please check your connection.');
}

Monitor webhook subscriptions

Regularly check your Automation webhook subscriptions

bash
curl -X GET "https://api.ugc.africa/automation/webhooks/subscriptions" \
  -H "Authorization: Bearer UGSK_live_xxx"

Use Webhooks Over Polling

Use webhooks for real-time updates instead of polling

javascript
// ❌ Don't poll for status
setInterval(async () => {
  const campaign = await getCampaign(campaignId);
  if (campaign.status === 'ACTIVE') {
    // Process...
  }
}, 5000);

//  Use webhooks
app.post('/webhooks/ugc', (req, res) => {
  if (req.body.trigger === 'campaign_funded') {
    // Process immediately
  }
  res.status(200).send('OK');
});

3. Campaign & slot tracking

Track campaign and slot status

Track a campaign from DRAFT through funding, activation, and slot fulfillment

javascript
const CAMPAIGN_STATUSES = {
  DRAFT: 'Created but not yet funded',
  PENDING_APPROVAL: 'Funded, awaiting moderation',
  ACTIVE: 'Live - creators can reserve slots',
  PAUSED: 'Temporarily paused',
  COMPLETED: 'Budget exhausted or end date reached',
};

const SLOT_STATUSES = {
  RESERVED: 'Creator reserved, broadcast window open',
  SUBMITTED: 'Creator submitted proof of post',
  APPROVED: 'Verified - payout accrues',
  REJECTED: 'Failed moderation',
  EXPIRED: 'Reservation window elapsed unfulfilled',
};

Handle status updates via webhooks

Process campaign/slot lifecycle updates from webhooks - see the

javascript
app.post('/webhooks/ugc', async (req, res) => {
  const { trigger, data } = req.body;

  switch (trigger) {
    case 'campaign_funded':
      await db.campaigns.update({ id: data.campaignId, status: 'ACTIVE' });
      break;

    case 'slot_approved':
      await db.slots.update({ id: data.slotId, status: 'APPROVED' });
      break;

    case 'earnings_released':
      await db.payouts.create({ ...data });
      break;
  }

  res.status(200).send('OK');
});

4. Webhook Implementation

Return 200 OK Immediately

Process webhook events asynchronously

javascript
app.post('/webhooks/ugc', express.raw({ type: 'application/json' }), async (req, res) => {
  if (!verifySignature(req.headers['x-ugc-signature'], req.body, webhookSecret)) {
    return res.status(401).send('Invalid signature');
  }
  res.status(200).send('OK');
  processWebhookAsync(JSON.parse(req.body)).catch((err) => console.error(err));
});

Implement Idempotency

Check if event already processed before handling

javascript
const processedEvents = new Set();

app.post('/webhooks/ugc', async (req, res) => {
  const eventId = req.headers['x-ugc-event-id'];

  if (processedEvents.has(eventId)) {
    return res.status(200).send('Already processed');
  }

  await handleEvent(req.body);
  processedEvents.add(eventId);

  res.status(200).send('OK');
});

Store Webhook Events

Log webhook events for audit trail

javascript
app.post('/webhooks/ugc', async (req, res) => {
  await db.webhookEvents.create({
    trigger: req.body.trigger,
    payload: JSON.stringify(req.body),
    receivedAt: new Date(),
    processed: false
  });

  res.status(200).send('OK');
});

Handle Retries

Automation retries failed deliveries automatically (up to 5 attempts)

javascript
app.post('/webhooks/ugc', async (req, res) => {
  try {
    await processWebhook(req.body);
    res.status(200).send('OK');
  } catch (error) {
    console.error('Webhook processing failed:', error);
    // Return non-2xx - Automation will retry
    res.status(500).send('Processing failed');
  }
});

5. User Experience

Show Loading States

Display loading indicators during payment processing

javascript
async function fundCampaign(campaignId, amount) {
  setLoading(true);

  try {
    const res = await fetch(`/api/automation/actions/campaigns/${campaignId}/fund`, {
      method: 'POST',
      body: JSON.stringify({ method: 'checkout', amount }),
    });
    const { data } = await res.json();
    window.location.href = data.checkoutUrl;
  } catch (error) {
    setError('Funding initiation failed');
  } finally {
    setLoading(false);
  }
}

Handle Redirects

Handle redirects gracefully after checkout

javascript
// Success page
app.get('/campaign-funded', async (req, res) => {
  const campaignId = req.query.campaignId;

  const campaign = await getCampaign(campaignId);

  if (campaign.status === 'ACTIVE' || campaign.status === 'PENDING_APPROVAL') {
    res.render('success', { campaign });
  } else {
    res.redirect('/fund');
  }
});

Provide Clear Messages

Show clear success/error messages to users

javascript
const USER_MESSAGES = {
  FUNDING_SUCCESS: 'Campaign funded! Creators can now reserve slots.',
  FUNDING_PENDING: 'Funding initiated. Complete payment to activate your campaign.',
  FUNDING_FAILED: 'Funding failed. Please try again.',
  SLOT_APPROVED: 'A creator\'s post was approved!',
  EARNINGS_RELEASED: 'Creator earnings cleared into the spendable wallet.',
  WITHDRAWAL_COMPLETED: 'A wallet cash-out was settled on the rail.'
};

6. Testing

Start Small

There is no sandbox mode - every key issued is live. Validate your integration with a small-budget real campaign before scaling up.

bash
# Your live key - active immediately, no separate test environment
UGC_API_KEY=UGSK_live_abc123...

Test All Flows

Test the full campaign lifecycle:

  • Campaign creation
  • Campaign funding
  • Slot reservation, submission, approval/rejection
  • Payout

Test Webhooks

Verify webhook delivery and signature verification

bash
# Subscribe a local tunnel, then trigger a real event (e.g. fund a test campaign)
curl -X POST https://api.ugc.africa/automation/webhooks/subscribe \
  -H "Authorization: Bearer UGSK_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{"trigger":"campaign_funded","targetUrl":"https://abc123.ngrok.io/webhooks/ugc"}'

Test Error Cases

Test error handling and edge cases

javascript
// Test insufficient funds
// Test invalid API key
// Test rate limiting
// Test network errors

Monitor Production

Monitor first few production transactions closely

Performance Optimization

Cache API Responses

Cache frequently accessed data

javascript
const cache = new Map();

async function getCampaign(campaignId) {
  const cacheKey = `campaign-${campaignId}`;

  if (cache.has(cacheKey)) {
    return cache.get(cacheKey);
  }

  const campaign = await fetchCampaign(campaignId);
  cache.set(cacheKey, campaign);

  return campaign;
}

Use Connection Pooling

Reuse HTTP connections

javascript
const axios = require('axios');
const http = require('http');
const https = require('https');

const instance = axios.create({
  httpAgent: new http.Agent({ keepAlive: true }),
  httpsAgent: new https.Agent({ keepAlive: true })
});

Implement Request Queuing

Queue requests to avoid rate limits

javascript
const Queue = require('bull');
const fundingQueue = new Queue('campaign-funding');

fundingQueue.process(async (job) => {
  return await fundCampaign(job.data.campaignId, job.data.amount);
});

Did this solve your problem?