Best Practices
Production-ready integration guidelines
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
# .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
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.
const apiKey = process.env.UGC_API_KEY; // UGSK_live_...Implement Retry Logic
Retry failed requests with exponential backoff
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.
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
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
// ❌ 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
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
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
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
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
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)
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
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
// 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
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.
# 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
# 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
// Test insufficient funds
// Test invalid API key
// Test rate limiting
// Test network errorsMonitor Production
Monitor first few production transactions closely
Performance Optimization
Cache API Responses
Cache frequently accessed data
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
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
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?