Skip to main content

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

Code Removed

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

Code Removed

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

Code Removed

Implementation details removed for security.

Contact support for implementation guidance.

Use Cases:

  • ✅ Automated CI/CD pipelines
  • ✅ Code review automation
  • ✅ Deployment workflows
  • ✅ Issue tracking integration

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

Code Removed

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

Code Removed

Implementation details removed for security.

Contact support for implementation guidance.

❌ Don't Do This

Code Removed

Implementation details removed for security.

Contact support for implementation guidance.


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