Skip to main content

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

FieldTypeDescription
CustomeridIntegerUnique customer account ID for API calls
CustomerCodeStringUnique identifier across all VenturEd Solutions (LEA + School code)
NameStringCustomer name with prefix indicating account type
ApiKeyStringAPI key to use as Password field in message requests
attributesStringPipe-separated key=value pairs of customer attributes
LogoStringURL 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 district
  • type: Account type (primary, secondary, etc.)
  • billing: Billing arrangement
  • plan: 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: