Advanced Callback Patterns
This guide covers advanced patterns for integrating external systems using callbacks, including payment gateways, third-party APIs, and webhook systems.
Overview
Advanced callback patterns provide:
- ✅ Payment gateway integration - Paystack, Flutterwave, Stripe
- ✅ Third-party API integration - Credit bureaus, KYC providers
- ✅ Webhook systems - GitHub, Slack, custom webhooks
- ✅ Retry and error handling - Automatic retry on failures
- ✅ Mobile app callbacks - Deep links and push notifications
Pattern 1: Paystack Payment Integration
Complete integration with Paystack payment gateway.
Backend: Payment Process
<bpmn:startEvent id="StartPayment" name="Start"/>
<!-- Initialize Paystack payment -->
<bpmn:serviceTask id="InitializePaystackPayment" name="Initialize Paystack">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="InitializePaystackPaymentCommand"/>
<custom:property name="amount" value="${context.amount * 100}"/> <!-- Kobo -->
<custom:property name="email" value="${context.customerEmail}"/>
<custom:property name="reference" value="${context.orderReference}"/>
<custom:property name="callbackUrl" value="https://api.yourbank.com/callbacks/paystack"/>
<custom:property name="metadata" value="${JSON.stringify({
orderId: context.orderId,
customerId: context.customerId,
productName: context.productName
})}"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:serviceTask>
<bpmn:scriptTask id="PreparePaystackCallback" name="Prepare">
<bpmn:script>
// Paystack returns authorization URL and reference
context.authorizationUrl = context.paystackResponse.data.authorization_url;
context.accessCode = context.paystackResponse.data.access_code;
context.paystackReference = context.paystackResponse.data.reference;
// Use Paystack reference as correlation key
context.correlationKey = context.paystackReference;
logger.info('Paystack initialized: ' + context.paystackReference);
return {
authorizationUrl: context.authorizationUrl,
reference: context.paystackReference,
status: 'PAYMENT_PENDING'
};
</bpmn:script>
</bpmn:scriptTask>
<!-- WAIT FOR PAYSTACK CALLBACK -->
<bpmn:receiveTask id="WaitForPaystackCallback" name="Wait for Paystack">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="CorrelationKey" value="${context.correlationKey}"/>
<custom:property name="CallbackType" value="paystack"/>
<custom:property name="TimeoutDuration" value="PT1H"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:receiveTask>
<!-- Timeout boundary -->
<bpmn:boundaryEvent id="PaystackTimeout"
attachedToRef="WaitForPaystackCallback"
cancelActivity="true">
<bpmn:timerEventDefinition>
<bpmn:timeDuration>PT1H</bpmn:timeDuration>
</bpmn:timerEventDefinition>
</bpmn:boundaryEvent>
<!-- Timeout: Cancel payment -->
<bpmn:scriptTask id="CancelPaystackPayment" name="Cancel Payment">
<bpmn:script>
logger.warn('Paystack payment timeout: ' + context.paystackReference);
context.paymentStatus = 'TIMEOUT';
context.orderStatus = 'CANCELLED';
return {
status: 'CANCELLED',
reason: 'PAYMENT_TIMEOUT'
};
</bpmn:script>
</bpmn:scriptTask>
<!-- Verify payment (security: always verify!) -->
<bpmn:serviceTask id="VerifyPaystackPayment" name="Verify Payment">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="VerifyPaystackPaymentCommand"/>
<custom:property name="reference" value="${context.paystackReference}"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:serviceTask>
<bpmn:scriptTask id="ProcessVerificationResult" name="Process Result">
<bpmn:script>
var verification = context.paystackVerification.data;
context.verifiedStatus = verification.status; // success, failed, abandoned
context.verifiedAmount = verification.amount / 100; // Convert from kobo
context.paidAt = verification.paid_at;
context.channel = verification.channel;
context.customerName = verification.customer.first_name + ' ' +
verification.customer.last_name;
// Validate amount
if (context.verifiedStatus === 'success' &&
context.verifiedAmount != context.amount) {
throw new BpmnError('AMOUNT_MISMATCH',
'Expected: ' + context.amount + ', Got: ' + context.verifiedAmount);
}
logger.info('Payment verified: ' + context.verifiedStatus +
', amount: ' + context.verifiedAmount);
return {
paymentStatus: context.verifiedStatus,
verifiedAmount: context.verifiedAmount,
paidAt: context.paidAt
};
</bpmn:script>
</bpmn:scriptTask>
<!-- Gateway: Payment successful? -->
<bpmn:exclusiveGateway id="PaymentSuccessful" name="Success?"/>
<bpmn:sequenceFlow sourceRef="PaymentSuccessful" targetRef="RecordPayment">
<bpmn:conditionExpression>${context.verifiedStatus === 'success'}</bpmn:conditionExpression>
</bpmn:sequenceFlow>
<bpmn:sequenceFlow sourceRef="PaymentSuccessful" targetRef="HandleFailedPayment">
<bpmn:conditionExpression>${context.verifiedStatus !== 'success'}</bpmn:conditionExpression>
</bpmn:sequenceFlow>
<!-- Record successful payment -->
<bpmn:serviceTask id="RecordPayment" name="Record Payment">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="RecordPaymentCommand"/>
<custom:property name="paymentType" value="PAYSTACK"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:serviceTask>
<bpmn:endEvent id="End"/>
Callback Endpoint
Implementation details removed for security.
Contact support for implementation guidance.
Mobile App Integration
// Mobile app (React Native)
const initiatePaystackPayment = async (orderId, amount) => {
try {
// Start workflow
const response = await fetch('https://api.yourbank.com/api/payments/initiate', {
method: 'POST',
headers: {
'Authorization': `Bearer ${userToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
workflowKey: 'PaystackPaymentProcess',
context: {
orderId: orderId,
amount: amount,
customerEmail: user.email,
customerId: user.id,
productName: 'Premium Subscription'
}
})
});
const result = await response.json();
// Open Paystack payment page
const authorizationUrl = result.context.authorizationUrl;
// Option 1: Open in WebView
navigation.navigate('PaymentWebView', {
url: authorizationUrl,
onSuccess: () => {
// Poll for payment status or wait for push notification
pollPaymentStatus(result.processInstanceId);
}
});
// Option 2: Open in browser
Linking.openURL(authorizationUrl);
} catch (error) {
console.error('Payment initialization error:', error);
Alert.alert('Error', 'Could not initialize payment');
}
};
// Poll for payment completion
const pollPaymentStatus = async (processInstanceId) => {
const maxAttempts = 60; // 5 minutes (5 second intervals)
let attempts = 0;
const interval = setInterval(async () => {
attempts++;
try {
const response = await fetch(
`https://api.yourbank.com/api/process/${processInstanceId}/status`
);
const status = await response.json();
if (status.state === 'COMPLETED') {
clearInterval(interval);
if (status.context.paymentStatus === 'success') {
Alert.alert('Success', 'Payment completed successfully!');
navigation.navigate('OrderSuccess', { orderId: status.context.orderId });
} else {
Alert.alert('Failed', 'Payment was not successful');
}
} else if (status.state === 'FAILED') {
clearInterval(interval);
Alert.alert('Error', 'Payment process failed');
} else if (attempts >= maxAttempts) {
clearInterval(interval);
Alert.alert('Timeout', 'Payment verification timeout');
}
} catch (error) {
console.error('Status poll error:', error);
}
}, 5000);
};
Key Features:
- ✅ Complete Paystack integration
- ✅ Webhook signature verification (security)
- ✅ Payment verification (double-check)
- ✅ Amount validation
- ✅ Mobile app flow
- ✅ Status polling
Pattern 2: Credit Bureau Integration (FirstCentral)
Asynchronous credit check with callback.
<bpmn:startEvent id="StartCreditCheck" name="Start"/>
<!-- Submit credit check request -->
<bpmn:serviceTask id="SubmitCreditCheckRequest" name="Submit Request">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="SubmitFirstCentralCreditCheckCommand"/>
<custom:property name="firstName" value="${context.customerFirstName}"/>
<custom:property name="lastName" value="${context.customerLastName}"/>
<custom:property name="dateOfBirth" value="${context.dateOfBirth}"/>
<custom:property name="bvn" value="${context.bvn}"/>
<custom:property name="callbackUrl" value="https://api.yourbank.com/callbacks/firstcentral"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:serviceTask>
<bpmn:scriptTask id="PrepareFirstCentralCallback" name="Prepare">
<bpmn:script>
// FirstCentral returns enquiry reference
context.enquiryReference = context.creditCheckResponse.enquiryReference;
context.correlationKey = context.enquiryReference;
logger.info('Credit check submitted: ' + context.enquiryReference);
return {
enquiryReference: context.enquiryReference,
status: 'CREDIT_CHECK_PENDING'
};
</bpmn:script>
</bpmn:scriptTask>
<!-- WAIT FOR CREDIT BUREAU CALLBACK -->
<bpmn:receiveTask id="WaitForCreditBureauResult" name="Wait for Result">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="CorrelationKey" value="${context.correlationKey}"/>
<custom:property name="CallbackType" value="credit_bureau"/>
<custom:property name="TimeoutDuration" value="PT10M"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:receiveTask>
<!-- Timeout boundary -->
<bpmn:boundaryEvent id="CreditCheckTimeout"
attachedToRef="WaitForCreditBureauResult"
cancelActivity="true">
<bpmn:timerEventDefinition>
<bpmn:timeDuration>PT10M</bpmn:timeDuration>
</bpmn:timerEventDefinition>
</bpmn:boundaryEvent>
<!-- Timeout: Retry or escalate -->
<bpmn:scriptTask id="HandleCreditCheckTimeout" name="Handle Timeout">
<bpmn:script>
context.retryCount = (context.retryCount || 0) + 1;
if (context.retryCount < 3) {
logger.warn('Credit check timeout, retry: ' + context.retryCount);
throw new BpmnError('RETRY_CREDIT_CHECK', 'Timeout, retrying...');
} else {
logger.error('Credit check timeout, max retries exceeded');
throw new BpmnError('CREDIT_CHECK_FAILED', 'Unable to get credit report');
}
</bpmn:script>
</bpmn:scriptTask>
<!-- Process credit report -->
<bpmn:scriptTask id="ProcessCreditReport" name="Process Report">
<bpmn:script>
var report = context.callbackData;
context.creditScore = report.creditScore;
context.riskRating = report.riskRating;
context.totalLoans = report.totalOutstandingLoans;
context.totalDelinquent = report.totalDelinquentAmount;
context.accountHistory = report.accountHistory;
// Decision logic
var approved = context.creditScore >= 650 &&
context.totalDelinquent == 0;
context.creditDecision = approved ? 'APPROVED' : 'DECLINED';
logger.info('Credit check complete: Score=' + context.creditScore +
', Decision=' + context.creditDecision);
return {
creditScore: context.creditScore,
riskRating: context.riskRating,
creditDecision: context.creditDecision
};
</bpmn:script>
</bpmn:scriptTask>
<bpmn:endEvent id="End"/>
Callback Endpoint with Retry
Implementation details removed for security.
Contact support for implementation guidance.
Key Features:
- ✅ Credit bureau integration
- ✅ Timeout with retry logic (3 attempts)
- ✅ Automatic credit decisioning
- ✅ API key authentication
- ✅ Comprehensive credit report processing
Pattern 3: GitHub Webhook Integration
Trigger workflows from GitHub events.
<bpmn:startEvent id="StartGitHubWebhook" name="Start"/>
<!-- Register GitHub webhook -->
<bpmn:serviceTask id="RegisterGitHubWebhook" name="Register Webhook">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="RegisterGitHubWebhookCommand"/>
<custom:property name="repository" value="${context.repositoryName}"/>
<custom:property name="events" value="['push', 'pull_request']"/>
<custom:property name="callbackUrl" value="https://api.yourbank.com/webhooks/github"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:serviceTask>
<bpmn:scriptTask id="PrepareGitHubCallback" name="Prepare">
<bpmn:script>
// Generate webhook ID
context.webhookId = 'GH-' + new Date().getTime();
context.correlationKey = context.webhookId;
// Store webhook ID in GitHub webhook config
context.webhookSecret = generateSecret();
logger.info('GitHub webhook registered: ' + context.webhookId);
return {
webhookId: context.webhookId,
webhookSecret: context.webhookSecret
};
function generateSecret() {
var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
var secret = '';
for (var i = 0; i < 32; i++) {
secret += chars.charAt(Math.floor(Math.random() * chars.length));
}
return secret;
}
</bpmn:script>
</bpmn:scriptTask>
<!-- WAIT FOR GITHUB EVENT -->
<bpmn:receiveTask id="WaitForGitHubEvent" name="Wait for Event">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="CorrelationKey" value="${context.correlationKey}"/>
<custom:property name="CallbackType" value="github_webhook"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:receiveTask>
<!-- Process GitHub event -->
<bpmn:scriptTask id="ProcessGitHubEvent" name="Process Event">
<bpmn:script>
var event = context.callbackData;
context.eventType = event.eventType; // push, pull_request
context.repository = event.repository.full_name;
context.branch = event.ref;
context.commits = event.commits;
context.author = event.pusher.name;
logger.info('GitHub event: ' + context.eventType +
', repository: ' + context.repository);
return {
eventType: context.eventType,
repository: context.repository,
commitCount: context.commits.length
};
</bpmn:script>
</bpmn:scriptTask>
<!-- Gateway: Event type? -->
<bpmn:exclusiveGateway id="EventType" name="Event Type?"/>
<bpmn:sequenceFlow sourceRef="EventType" targetRef="HandlePush">
<bpmn:conditionExpression>${context.eventType === 'push'}</bpmn:conditionExpression>
</bpmn:sequenceFlow>
<bpmn:sequenceFlow sourceRef="EventType" targetRef="HandlePullRequest">
<bpmn:conditionExpression>${context.eventType === 'pull_request'}</bpmn:conditionExpression>
</bpmn:sequenceFlow>
<!-- Handle push event -->
<bpmn:serviceTask id="HandlePush" name="Trigger Build">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="TriggerCIBuildCommand"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:serviceTask>
<!-- Handle pull request -->
<bpmn:serviceTask id="HandlePullRequest" name="Run Code Review">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="TriggerCodeReviewCommand"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:serviceTask>
<bpmn:endEvent id="End"/>
GitHub Webhook Endpoint
Implementation details removed for security.
Contact support for implementation guidance.
Use Cases:
- ✅ Automated CI/CD pipelines
- ✅ Code review automation
- ✅ Deployment workflows
- ✅ Issue tracking integration
Pattern 4: Mobile Deep Link Callbacks
Mobile app callbacks using deep links.
Process Definition
<bpmn:startEvent id="StartMobileCallback" name="Start"/>
<!-- Send push notification with deep link -->
<bpmn:serviceTask id="SendPushWithDeepLink" name="Send Push Notification">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="ConnectorKey" value="SendPushNotificationCommand"/>
<custom:property name="deviceTokens" value="${context.deviceTokens}"/>
<custom:property name="title" value="Action Required"/>
<custom:property name="body" value="Please approve your loan application"/>
<custom:property name="deepLink" value="bankapp://approve/${context.applicationId}"/>
<custom:property name="data" value="${JSON.stringify({
correlationKey: context.correlationKey,
applicationId: context.applicationId
})}"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:serviceTask>
<bpmn:scriptTask id="PrepareMobileCallback" name="Prepare">
<bpmn:script>
// Generate correlation key
context.correlationKey = 'MOBILE-' + new Date().getTime();
logger.info('Waiting for mobile callback: ' + context.correlationKey);
return {
correlationKey: context.correlationKey,
status: 'WAITING_FOR_USER'
};
</bpmn:script>
</bpmn:scriptTask>
<!-- WAIT FOR MOBILE APP CALLBACK -->
<bpmn:receiveTask id="WaitForMobileCallback" name="Wait for User Action">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="CorrelationKey" value="${context.correlationKey}"/>
<custom:property name="CallbackType" value="mobile_action"/>
<custom:property name="TimeoutDuration" value="P1D"/> <!-- 1 day -->
</custom:properties>
</bpmn:extensionElements>
</bpmn:receiveTask>
<!-- Process user action -->
<bpmn:scriptTask id="ProcessUserAction" name="Process Action">
<bpmn:script>
var action = context.callbackData;
context.userAction = action.action; // APPROVED, REJECTED
context.actionTimestamp = action.timestamp;
context.comments = action.comments;
logger.info('User action: ' + context.userAction);
return {
userAction: context.userAction,
comments: context.comments
};
</bpmn:script>
</bpmn:scriptTask>
<bpmn:endEvent id="End"/>
Mobile App Handler
// Mobile app (React Native) - Deep link handler
import { Linking } from 'react-native';
import PushNotification from 'react-native-push-notification';
// Configure deep links
const linking = {
prefixes: ['bankapp://'],
config: {
screens: {
ApproveApplication: 'approve/:applicationId'
}
}
};
// Handle deep link
useEffect(() => {
Linking.addEventListener('url', handleDeepLink);
return () => {
Linking.removeEventListener('url', handleDeepLink);
};
}, []);
const handleDeepLink = async ({ url }) => {
// Parse deep link: bankapp://approve/12345
const match = url.match(/bankapp:\/\/approve\/(.+)/);
if (match) {
const applicationId = match[1];
// Navigate to approval screen
navigation.navigate('ApproveApplication', { applicationId });
}
};
// Approval screen
const ApproveApplicationScreen = ({ route }) => {
const { applicationId } = route.params;
const [loading, setLoading] = useState(false);
const handleApprove = async (comments) => {
setLoading(true);
try {
// Send callback to API
const response = await fetch(
'https://api.yourbank.com/callbacks/mobile-action',
{
method: 'POST',
headers: {
'Authorization': `Bearer ${userToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
correlationKey: applicationId,
action: 'APPROVED',
comments: comments,
timestamp: new Date().toISOString()
})
}
);
if (response.ok) {
Alert.alert('Success', 'Application approved successfully!');
navigation.goBack();
} else {
Alert.alert('Error', 'Could not approve application');
}
} catch (error) {
console.error('Approval error:', error);
Alert.alert('Error', 'Network error');
} finally {
setLoading(false);
}
};
const handleReject = async (reason) => {
// Similar to handleApprove, but action: 'REJECTED'
};
return (
<View>
<Text>Application ID: {applicationId}</Text>
<TextInput placeholder="Comments" />
<Button title="Approve" onPress={handleApprove} disabled={loading} />
<Button title="Reject" onPress={handleReject} disabled={loading} />
</View>
);
};
Mobile Callback Endpoint
Implementation details removed for security.
Contact support for implementation guidance.
Key Features:
- ✅ Push notification with deep link
- ✅ Mobile app navigation
- ✅ User authentication
- ✅ Secure callback with auth token
- ✅ Rich user actions (approve/reject with comments)
Error Handling and Retry
Automatic Retry Pattern
<!-- Subprocess with retry logic -->
<bpmn:subProcess id="RetryableCallback" name="Retryable Callback">
<bpmn:startEvent id="RetryStart"/>
<!-- Attempt callback -->
<bpmn:receiveTask id="WaitForCallback" name="Wait">
<bpmn:extensionElements>
<custom:properties>
<custom:property name="CorrelationKey" value="${context.correlationKey}"/>
<custom:property name="TimeoutDuration" value="PT5M"/>
</custom:properties>
</bpmn:extensionElements>
</bpmn:receiveTask>
<!-- Timeout boundary -->
<bpmn:boundaryEvent id="CallbackTimeout"
attachedToRef="WaitForCallback"
cancelActivity="true">
<bpmn:timerEventDefinition>
<bpmn:timeDuration>PT5M</bpmn:timeDuration>
</bpmn:timerEventDefinition>
</bpmn:boundaryEvent>
<!-- Retry logic -->
<bpmn:scriptTask id="CheckRetry" name="Check Retry">
<bpmn:script>
context.retryCount = (context.retryCount || 0) + 1;
if (context.retryCount < 3) {
logger.warn('Callback timeout, retry: ' + context.retryCount);
throw new BpmnError('RETRY', 'Retrying...');
} else {
logger.error('Max retries exceeded');
throw new BpmnError('MAX_RETRIES_EXCEEDED', 'Callback failed');
}
</bpmn:script>
</bpmn:scriptTask>
<bpmn:endEvent id="RetryEnd"/>
</bpmn:subProcess>
<!-- Retry boundary (non-interrupting) -->
<bpmn:boundaryEvent id="RetryBoundary"
attachedToRef="RetryableCallback"
cancelActivity="false">
<bpmn:errorEventDefinition errorRef="RETRY"/>
</bpmn:boundaryEvent>
<!-- Wait before retry -->
<bpmn:intermediateCatchEvent id="WaitBeforeRetry" name="Wait 30s">
<bpmn:timerEventDefinition>
<bpmn:timeDuration>PT30S</bpmn:timeDuration>
</bpmn:timerEventDefinition>
</bpmn:intermediateCatchEvent>
<!-- Loop back to retry -->
<bpmn:sequenceFlow sourceRef="WaitBeforeRetry" targetRef="RetryableCallback"/>
Best Practices
✅ Do This
Implementation details removed for security.
Contact support for implementation guidance.
⌠Don't Do This
Implementation details removed for security.
Contact support for implementation guidance.
Related Documentation
- Callbacks - Callback fundamentals
- Receive Task - Receive task basics
- Timer Events - Callback timeouts
- Mobile Patterns - Mobile app integration
Features Used:
- Phase 3: Callbacks
- Phase 4: Timer Events (timeouts)
- Phase 5: Error Handling (retry logic)
Status: ✅ Production Ready
Version: 2.0
Last Updated: January 2026