Partner Authorization Callbacks
Receive customer API keys when customers authorize your partner application to send messages on their behalf.
Overview
When you register as a partner, you provide a Partner Endpoint URL. This URL will receive POST requests whenever a customer authorizes your application to send messages on their behalf.
Partner Setup Required
This guide applies to partner integrations. If you're integrating directly with a single customer, see Customer Authentication instead.
Callback Setup
During partner registration, you configure:
- Partner Endpoint URL: Where authorization callbacks will be sent
- Business Information: Legal and billing details
- Technical Contact: Developer information
Authorization Callback Payload
When a customer authorizes your partner application, you'll receive a POST request with this payload:
{
"Customerid": 1234,
"CustomerCode": "3281234",
"Name": "Customer: Example School",
"ApiKey": "example-api-key-67890",
"attributes": "region=north|type=primary",
"Logo": "https://example.com/school-logo.png"
}
Payload Fields
Field | Type | Description |
---|---|---|
Customerid | Integer | Unique customer account ID for API calls |
CustomerCode | String | Unique identifier across all VenturEd Solutions (LEA + School code) |
Name | String | Customer name with prefix indicating account type |
ApiKey | String | API key to use as Password field in message requests |
attributes | String | Pipe-separated key=value pairs of customer attributes |
Logo | String | URL to customer logo (may be empty) |
Account Type Indicators
The Name
field prefix indicates the account type:
"Customer: School Name"
- Dedicated communications account for this customer"Partner: Partner Name"
- Customer uses partner's generic sending account (no dedicated account)
Attributes Format
The attributes
field contains pipe-separated key=value pairs:
"region=north|type=primary|billing=monthly"
Common attributes:
region
: Geographic region or districttype
: Account type (primary, secondary, etc.)billing
: Billing arrangementplan
: Service plan level
Implementation Example
Node.js/Express Handler
app.post('/partner/authorization-callback', (req, res) => {
try {
const authData = req.body;
// Validate required fields
if (!authData.Customerid || !authData.ApiKey) {
return res.status(400).send('Invalid payload');
}
// Determine account type
const isPartnerAccount = authData.Name.startsWith('Partner:');
const isDedicatedAccount = authData.Name.startsWith('Customer:');
// Parse attributes
const attributes = {};
if (authData.attributes) {
authData.attributes.split('|').forEach(attr => {
const [key, value] = attr.split('=');
if (key && value) {
attributes[key] = value;
}
});
}
// Store customer credentials securely
await storeCustomerCredentials({
customerId: authData.Customerid,
customerCode: authData.CustomerCode,
apiKey: authData.ApiKey,
name: authData.Name,
logo: authData.Logo,
attributes: attributes,
accountType: isPartnerAccount ? 'partner' : 'dedicated',
authorizedAt: new Date()
});
// Acknowledge receipt (always return 200 OK)
res.status(200).send('OK');
console.log(`Customer ${authData.Customerid} authorized: ${authData.Name}`);
} catch (error) {
console.error('Authorization callback error:', error);
res.status(200).send('OK'); // Still acknowledge to prevent retries
}
});
Python/Flask Handler
from flask import Flask, request, jsonify
import json
import logging
app = Flask(__name__)
@app.route('/partner/authorization-callback', methods=['POST'])
def handle_authorization_callback():
try:
auth_data = request.get_json()
# Validate required fields
if not auth_data.get('Customerid') or not auth_data.get('ApiKey'):
return 'Invalid payload', 400
# Parse attributes
attributes = {}
if auth_data.get('attributes'):
for attr in auth_data['attributes'].split('|'):
if '=' in attr:
key, value = attr.split('=', 1)
attributes[key] = value
# Determine account type
account_type = 'partner' if auth_data['Name'].startswith('Partner:') else 'dedicated'
# Store credentials (implement your storage logic)
store_customer_credentials({
'customer_id': auth_data['Customerid'],
'customer_code': auth_data['CustomerCode'],
'api_key': auth_data['ApiKey'],
'name': auth_data['Name'],
'logo': auth_data.get('Logo', ''),
'attributes': attributes,
'account_type': account_type,
'authorized_at': datetime.now()
})
logging.info(f"Customer {auth_data['Customerid']} authorized: {auth_data['Name']}")
return 'OK', 200
except Exception as error:
logging.error(f"Authorization callback error: {error}")
return 'OK', 200 # Always acknowledge to prevent retries
C# ASP.NET Handler
[HttpPost("partner/authorization-callback")]
public async Task<IActionResult> HandleAuthorizationCallback([FromBody] AuthorizationCallbackDto authData)
{
try
{
// Validate required fields
if (authData.Customerid == 0 || string.IsNullOrEmpty(authData.ApiKey))
{
return BadRequest("Invalid payload");
}
// Parse attributes
var attributes = new Dictionary<string, string>();
if (!string.IsNullOrEmpty(authData.Attributes))
{
foreach (var attr in authData.Attributes.Split('|'))
{
var parts = attr.Split('=', 2);
if (parts.Length == 2)
{
attributes[parts[0]] = parts[1];
}
}
}
// Determine account type
var accountType = authData.Name.StartsWith("Partner:") ? "partner" : "dedicated";
// Store customer credentials
await _customerService.StoreCredentialsAsync(new CustomerCredentials
{
CustomerId = authData.Customerid,
CustomerCode = authData.CustomerCode,
ApiKey = authData.ApiKey,
Name = authData.Name,
Logo = authData.Logo,
Attributes = attributes,
AccountType = accountType,
AuthorizedAt = DateTime.UtcNow
});
_logger.LogInformation("Customer {CustomerId} authorized: {Name}",
authData.Customerid, authData.Name);
return Ok("OK");
}
catch (Exception ex)
{
_logger.LogError(ex, "Authorization callback error");
return Ok("OK"); // Always acknowledge to prevent retries
}
}
public class AuthorizationCallbackDto
{
public int Customerid { get; set; }
public string CustomerCode { get; set; }
public string Name { get; set; }
public string ApiKey { get; set; }
public string Attributes { get; set; }
public string Logo { get; set; }
}
Security Considerations
Endpoint Security
// Add basic security measures
app.use('/partner/authorization-callback', (req, res, next) => {
// Verify request comes from expected source
const allowedIPs = ['185.xxx.xxx.xxx']; // VenturEd IP ranges
const clientIP = req.ip || req.connection.remoteAddress;
if (process.env.NODE_ENV === 'production' && !allowedIPs.includes(clientIP)) {
return res.status(403).send('Forbidden');
}
// Verify Content-Type
if (req.get('Content-Type') !== 'application/json') {
return res.status(400).send('Invalid Content-Type');
}
next();
});
Credential Storage
async function storeCustomerCredentials(credentialData) {
// Encrypt sensitive data before storage
const encryptedApiKey = encrypt(credentialData.apiKey);
// Store in secure database
await database.customers.upsert({
customer_id: credentialData.customerId,
customer_code: credentialData.customerCode,
encrypted_api_key: encryptedApiKey,
name: credentialData.name,
logo_url: credentialData.logo,
attributes: JSON.stringify(credentialData.attributes),
account_type: credentialData.accountType,
authorized_at: credentialData.authorizedAt,
updated_at: new Date()
});
// Invalidate any cached credentials
cache.del(`customer_${credentialData.customerId}`);
// Log authorization (without sensitive data)
audit.log('customer_authorized', {
customer_id: credentialData.customerId,
customer_code: credentialData.customerCode,
account_type: credentialData.accountType
});
}
Testing Authorization Callbacks
Local Development with ngrok
# Install ngrok for local testing
npm install -g ngrok
# Expose local server
ngrok http 3000
# Use the ngrok URL as your Partner Endpoint URL
# https://abc123.ngrok.io/partner/authorization-callback
Callback Validation
// Test callback handler with sample payload
const testCallback = async () => {
const samplePayload = {
Customerid: 1234,
CustomerCode: "3281234",
Name: "Customer: Test School",
ApiKey: "test-api-key-12345",
attributes: "region=test|type=demo",
Logo: ""
};
const response = await fetch('http://localhost:3000/partner/authorization-callback', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(samplePayload)
});
console.log('Callback response:', response.status, await response.text());
};
Troubleshooting
Common Issues
Callback Not Received
- Verify Partner Endpoint URL is correct and accessible
- Check firewall settings allow incoming POST requests
- Ensure endpoint returns 200 OK status
- Test with a tool like ngrok for local development
Invalid Payload
- Check request Content-Type is application/json
- Verify all required fields are present
- Log raw request body for debugging
Duplicate Callbacks
- Customers may re-authorize, sending duplicate callbacks
- Implement idempotent handling using customer_id
- Update existing records rather than creating duplicates
Monitoring and Alerting
// Monitor callback health
const callbackMetrics = {
total: 0,
successful: 0,
failed: 0,
lastReceived: null
};
app.post('/partner/authorization-callback', (req, res) => {
callbackMetrics.total++;
callbackMetrics.lastReceived = new Date();
try {
// Handle callback logic
handleAuthorizationCallback(req.body);
callbackMetrics.successful++;
res.status(200).send('OK');
} catch (error) {
callbackMetrics.failed++;
// Log error but still acknowledge
console.error('Callback processing failed:', error);
res.status(200).send('OK');
}
});
// Health check endpoint
app.get('/partner/callback-health', (req, res) => {
res.json({
status: 'healthy',
metrics: callbackMetrics,
uptime: process.uptime()
});
});
Related Documentation: