# Aslan Partner API Documentation ## Overview The Aslan Partner API is a RESTful API designed for payroll providers to integrate with Aslan's earned wage access platform. This API enables seamless management of company registrations, employee onboarding, and payroll deduction reporting. **Base URL:** `https://partner.aslan.io/api/v1` **Note:** All endpoints in this documentation are relative to the base URL above. ## Table of Contents 1. [Getting Started](#getting-started) 2. [Authentication](#authentication) 3. [What You Can Do](#what-you-can-do) 4. [Webhooks](#webhooks) 5. [Request & Response Format](#request--response-format) 6. [Error Handling](#error-handling) 7. [Rate Limiting](#rate-limiting) 8. [Support](#support) ## Getting Started ### Prerequisites Before you begin, you will need: - **API Key** - Provided by Aslan - **API Secret** - Provided by Aslan - **Company ID(s)** - The UUID(s) of the companies you manage ### Quick Start 1. **Obtain your API credentials** from Aslan (API Key and API Secret will be provided securely) 2. **Implement the authentication** flow using the code examples provided in this documentation 3. **Test your integration** by making a GET request to `/companies` to retrieve your company list 4. **Review the API reference** documentation for detailed endpoint specifications, schemas, and examples 5. **Integrate the endpoints** relevant to your payroll workflow (company sync, employee onboarding, deduction reporting) ## Authentication All API requests must be authenticated using your **API Key** and **API Secret**. ### Authentication Method The API uses **Bearer Token authentication** with HMAC-SHA256 signing to ensure request authenticity and prevent replay attacks. #### How to Authenticate 1. Create a signature string in the format: `METHOD\nPATH\nTIMESTAMP` (note the trailing newline) 2. Generate an HMAC-SHA256 hash of the signature string using your API Secret as the key 3. Base64 encode the resulting hash 4. Include the base64-encoded signature in the `Authorization` header #### Required Headers ``` Authorization: Bearer {YOUR_API_KEY}:{BASE64_ENCODED_SIGNATURE} X-Aslan-Timestamp: {UNIX_TIMESTAMP_IN_SECONDS} Content-Type: application/json ``` #### Important Security Notes - **Timestamp Validation**: Requests must be sent within 5 minutes of the timestamp to prevent replay attacks - **Signature Components**: The signature string must be exactly `METHOD\nPATH\nTIMESTAMP` with literal newline characters (`\n`) - **Path Format**: Use the full request path including query parameters (e.g., `/api/v1/companies?page=0`) - **Case Sensitivity**: HTTP method must be uppercase (e.g., `GET`, `POST`, `PUT`) #### Example (Node.js) ```javascript const crypto = require('crypto'); const axios = require('axios'); const API_KEY = 'your-api-key'; const API_SECRET = 'your-api-secret'; const BASE_URL = 'https://partner.aslan.io'; /** * Generate HMAC-SHA256 signature for authentication * @param {string} method - HTTP method (GET, POST, PUT, etc.) * @param {string} path - Request path including query parameters * @param {number} timestamp - Unix timestamp in seconds * @returns {string} Base64-encoded signature */ function generateSignature(method, path, timestamp) { // Signature format: METHOD\nPATH\nTIMESTAMP const signatureString = `${method}\n${path}\n${timestamp}`; const hmac = crypto.createHmac('sha256', API_SECRET); hmac.update(signatureString); const signature = hmac.digest('base64'); return signature; } /** * Make an authenticated request to the Aslan Partner API */ async function makeAuthenticatedRequest(method, path, data = null) { const timestamp = Math.floor(Date.now() / 1000); const signature = generateSignature(method, path, timestamp); const headers = { 'Authorization': `Bearer ${API_KEY}:${signature}`, 'X-Aslan-Timestamp': timestamp.toString(), 'Content-Type': 'application/json' }; const config = { method, url: `${BASE_URL}${path}`, headers, ...(data && {data}) }; return await axios(config); } // Example: Get all companies makeAuthenticatedRequest('GET', '/api/v1/companies') .then(response => console.log(response.data)) .catch(error => console.error(error.response?.data || error.message)); // Example: Create a new user const newUser = { firstName: 'John', lastName: 'Doe', email: 'john.doe@example.com', salaried: true }; makeAuthenticatedRequest('POST', '/api/v1/companies/{companyId}/users', newUser) .then(response => console.log('User created:', response.data)) .catch(error => console.error(error.response?.data || error.message)); ``` #### Example (Python) ```python import hmac import hashlib import base64 import time import requests import json # Configuration API_KEY = 'your-api-key' API_SECRET = 'your-api-secret' BASE_URL = 'https://partner.aslan.io' def generate_signature(method, path, timestamp): """ Generate HMAC-SHA256 signature for authentication Args: method: HTTP method (GET, POST, PUT, etc.) path: Request path including query parameters timestamp: Unix timestamp in seconds (as string) Returns: Base64-encoded signature """ # Signature format: METHOD\nPATH\nTIMESTAMP (note trailing newline) signature_string = f"{method}\n{path}\n{timestamp}" # Generate HMAC-SHA256 signature hmac_signature = hmac.new( key=API_SECRET.encode('utf-8'), msg=signature_string.encode('utf-8'), digestmod=hashlib.sha256 ).digest() # Encode to base64 signature = base64.b64encode(hmac_signature).decode('utf-8') return signature def make_authenticated_request(method, path, data=None): """Make an authenticated request to the Aslan Partner API""" timestamp = str(int(time.time())) signature = generate_signature(method, path, timestamp) url = f"{BASE_URL}{path}" headers = { 'Authorization': f'Bearer {API_KEY}:{signature}', 'X-Aslan-Timestamp': timestamp, 'Content-Type': 'application/json' } if method == 'GET': response = requests.get(url, headers=headers) elif method == 'POST': response = requests.post(url, headers=headers, json=data) elif method == 'PUT': response = requests.put(url, headers=headers, json=data) else: raise ValueError(f"Unsupported method: {method}") return response # Example: Get all companies try: response = make_authenticated_request('GET', '/api/v1/companies') print(f"Status: {response.status_code}") print(f"Response: {response.json()}") except requests.exceptions.RequestException as e: print(f"Error: {e}") # Example: Create a new user new_user = { 'firstName': 'John', 'lastName': 'Doe', 'email': 'john.doe@example.com', 'salaried': True } try: response = make_authenticated_request( 'POST', '/api/v1/companies/{companyId}/users', data=new_user ) print(f"User created: {response.json()}") except requests.exceptions.RequestException as e: print(f"Error: {e}") ``` ## What You Can Do The Aslan Partner API enables payroll providers to seamlessly integrate with Aslan's earned wage access platform. The API provides the following capabilities: ### Company Management Manage companies registered with Aslan: - Retrieve your company portfolio with flexible pagination and filtering - Access detailed company information and registration status - Update company details as they change in your payroll system ### User Management Manage employees enrolled in Aslan: - Create employee records for workers eligible for earned wage access - Retrieve employee lists with pagination, search, and filtering capabilities - Access detailed employee information including employment status and verification details - Update employee information to keep records synchronized with your payroll system - Send invitation emails to employees to join the Aslan platform ### Deduction Reports Access payroll deduction information: - Retrieve company-level deduction reports for payroll processing - Access individual employee deduction details for specific reporting periods ### Key Features - **Flexible Pagination**: All list endpoints support configurable page sizes and navigation - **Powerful Filtering**: Filter data by status, dates, and other relevant criteria - **Search Capabilities**: Search for employees by name or email - **Sorting Options**: Sort results in ascending or descending order - **Comprehensive Error Handling**: Receive detailed validation errors to quickly resolve issues ### Integration Workflow 1. **Company Setup**: Companies are typically pre-registered by Aslan. Use the API to retrieve company information and verify registration status. 2. **Employee Onboarding**: Employee creation is a two-step process: - First, create the employee record with their employment details - Then, send an invitation when you're ready for them to join Aslan - This separation gives you control over the invitation timing 3. **Ongoing Synchronization**: Keep employee records up-to-date by updating information as it changes in your payroll system. 4. **Deduction Processing**: Retrieve deduction reports during your payroll cycles to process earned wage access repayments. ### Important Considerations - **Employee Roles**: The Partner API is designed for managing standard employees. Administrative roles are managed separately by Aslan. - **Status Lifecycle**: Employee status transitions (e.g., invited → active) are managed by Aslan based on employee actions. You can view status but not directly modify it. - **Data Validation**: All requests are validated against strict schemas. Refer to error responses for detailed validation feedback. For complete endpoint specifications, request/response schemas, and field-level documentation, please refer to the API Reference. ## Webhooks Aslan uses webhooks to notify your system about important events in real-time. When significant events occur (such as deductions reports being generated), Aslan will send an HTTP POST request to your configured webhook endpoint(s). ### Overview Webhooks enable you to: - Receive real-time notifications when deductions reports are ready for download - Automate your payroll processing workflows - Reduce the need for frequent polling of the API - Ensure timely processing of employee deductions ### Webhook Setup To receive webhooks, you need to: 1. **Provide Webhook URL(s)**: Contact your Aslan account manager to configure one or more webhook endpoints 2. **Implement Webhook Handler**: Create an HTTPS endpoint that can receive POST requests 3. **Verify Signatures**: Validate the HMAC signature on each webhook to ensure authenticity 4. **Return 200 OK**: Respond with HTTP 200 within 5 seconds to acknowledge receipt **Important**: Your webhook endpoint must: - Be publicly accessible over HTTPS (not HTTP) - Respond within 5 seconds to avoid timeout - Return HTTP status code 200 to indicate successful receipt - Be idempotent (handle duplicate deliveries gracefully) ### Webhook Security All webhooks are signed using HMAC-SHA256 to ensure they originated from Aslan and haven't been tampered with. #### Signature Verification Each webhook request includes two headers for verification: ``` X-Aslan-Signature: {BASE64_ENCODED_HMAC_SIGNATURE} X-Aslan-Timestamp: {UNIX_TIMESTAMP_IN_SECONDS} ``` **Signature Calculation:** The signature is calculated using the same API Secret used for authenticating your API requests. ``` Signature String Format: "METHOD\nPATH\nTIMESTAMP\nJSON_BODY\n" ``` **Key differences from API authentication:** - Includes the **full JSON body** in the signature (not present in API request signatures) - Uses POST as the HTTP method - Path is the endpoint path of your webhook URL (e.g., `/webhooks/aslan`) #### Verification Example (Node.js) ```javascript const crypto = require('crypto'); const express = require('express'); const API_SECRET = 'your-api-secret'; // Same secret used for API authentication function verifyWebhookSignature(req) { const receivedSignature = req.headers['x-aslan-signature']; const timestamp = req.headers['x-aslan-timestamp']; const jsonBody = JSON.stringify(req.body); // Extract path from request URL const path = new URL(req.url, `http://${req.headers.host}`).pathname; // Build signature string: "POST\n/path\n1234567890\n{json}\n" const signatureString = `POST\n${path}\n${timestamp}\n${jsonBody}\n`; // Calculate expected signature const hmac = crypto.createHmac('sha256', API_SECRET); hmac.update(signatureString); const expectedSignature = hmac.digest('base64'); // Use timing-safe comparison return crypto.timingSafeEqual( Buffer.from(receivedSignature), Buffer.from(expectedSignature) ); } // Webhook endpoint handler app.post('/webhooks/aslan', express.json(), (req, res) => { // 1. Verify signature if (!verifyWebhookSignature(req)) { console.error('Invalid webhook signature'); return res.status(401).json({error: 'Invalid signature'}); } // 2. Verify timestamp (prevent replay attacks) const timestamp = parseInt(req.headers['x-aslan-timestamp']); const now = Math.floor(Date.now() / 1000); if (Math.abs(now - timestamp) > 300) { // 5 minutes tolerance console.error('Webhook timestamp too old'); return res.status(401).json({error: 'Timestamp expired'}); } // 3. Process webhook event const event = req.body; console.log('Received webhook:', event.eventType); // Process event asynchronously (don't block response) processWebhookEvent(event).catch(err => { console.error('Error processing webhook:', err); }); // 4. Return 200 OK immediately res.status(200).json({received: true}); }); ``` #### Verification Example (Python) ```python import hmac import hashlib import base64 import time import json from flask import Flask, request, jsonify API_SECRET = 'your-api-secret' # Same secret used for API authentication app = Flask(__name__) def verify_webhook_signature(request): """Verify HMAC signature on webhook request""" received_signature = request.headers.get('X-Aslan-Signature') timestamp = request.headers.get('X-Aslan-Timestamp') if not received_signature or not timestamp: return False # Get JSON body as string json_body = request.get_data(as_text=True) # Extract path from request path = request.path # Build signature string: "POST\n/path\n1234567890\n{json}\n" signature_string = f"POST\n{path}\n{timestamp}\n{json_body}\n" # Calculate expected signature hmac_signature = hmac.new( key=API_SECRET.encode('utf-8'), msg=signature_string.encode('utf-8'), digestmod=hashlib.sha256 ).digest() expected_signature = base64.b64encode(hmac_signature).decode('utf-8') # Timing-safe comparison return hmac.compare_digest(received_signature, expected_signature) @app.route('/webhooks/aslan', methods=['POST']) def handle_webhook(): # 1. Verify signature if not verify_webhook_signature(request): return jsonify({'error': 'Invalid signature'}), 401 # 2. Verify timestamp (prevent replay attacks) timestamp = int(request.headers.get('X-Aslan-Timestamp')) now = int(time.time()) if abs(now - timestamp) > 300: # 5 minutes tolerance return jsonify({'error': 'Timestamp expired'}), 401 # 3. Process webhook event event = request.get_json() print(f"Received webhook: {event['eventType']}") # Process event asynchronously (don't block response) # You might want to queue this for background processing process_webhook_event(event) # 4. Return 200 OK immediately return jsonify({'received': True}), 200 ``` ### Webhook Events #### Event Structure All webhooks follow a consistent structure: ```json { "eventId": "a1b2c3d4-e5f6-4a8b-9c0d-e1f2a3b4c5d6", "eventType": "deductions_report_generated", "timestamp": 1736611200, "body": { // Event-specific payload (see below) } } ``` **Common Fields:** | Field | Type | Description | | --- | --- | --- | | `eventId` | UUID | Unique identifier for this webhook event (use for deduplication) | | `eventType` | String | Type of event (see event types below) | | `timestamp` | Integer | Unix timestamp (seconds) when the event occurred | | `body` | Object | Event-specific payload containing details | #### Event Types ##### `deductions_report_generated` Sent when a deductions report has been generated and is ready for processing. This webhook provides a summary of all employee deductions for the pay period. **Event Body:** ```json { "companyId": "a3f2e1d0-5c4b-4a3e-9f8e-7d6c5b4a3e2f", "reportId": "c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f", "employeeDeductions": [ { "employeeId": "f7e6d5c4-b3a2-4918-8e7d-6c5b4a3f2e1d", "reportId": "e1f2a3b4-c5d6-4e7f-8a9b-0c1d2e3f4a5b", "from": "2024-01-01", "to": "2024-01-31", "deductionAmount": 250.00 }, { "employeeId": "a9b8c7d6-e5f4-4a3b-2c1d-0e9f8a7b6c5d", "reportId": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e", "from": "2024-01-01", "to": "2024-01-31", "deductionAmount": 175.50 } ] } ``` **Root Level Fields:** | Field | Type | Description | | --- | --- | --- | | `companyId` | UUID | Unique identifier for the company | | `reportId` | UUID | Unique identifier for this deductions report | | `employeeDeductions` | Array | List of individual employee deduction records | **Employee Deduction Fields:** | Field | Type | Description | | --- | --- | --- | | `employeeId` | UUID | Unique identifier for the employee | | `reportId` | UUID | Unique identifier for this employee's deduction report | | `from` | Date (YYYY-MM-DD) | Start date of the deduction period | | `to` | Date (YYYY-MM-DD) | End date of the deduction period | | `deductionAmount` | Decimal | Amount to deduct from this employee's salary (in GBP) | **Important Notes:** - Each employee in the `employeeDeductions` array has their own `reportId` (different from the top-level `reportId`) - The `from` and `to` dates represent the pay period covered by the deductions - All amounts are in GBP (Great British Pounds) - The `deductionAmount` is the total to deduct for that employee for the entire period - Employees with zero deductions may not appear in the array **Use Case:** When you receive this webhook: 1. Iterate through the `employeeDeductions` array 2. For each employee, record the `deductionAmount` for the pay period (`from` to `to`) 3. Process the deductions in your payroll system during the next payroll run 4. Store the top-level `eventId` from the webhook envelope to avoid processing duplicates 5. Optionally store each employee's `reportId` for audit trail purposes ### Webhook Delivery #### Retry Behavior Aslan implements automatic retries for failed webhook deliveries: - **Maximum Attempts**: 3 - **Retry Schedule**: - Attempt 1: Immediate - Attempt 2: 1 second after first failure - Attempt 3: 3 seconds after second failure (2s backoff × 1.5 multiplier) - **Timeout**: 5 seconds per attempt - **Success Criteria**: HTTP 200 OK status code **What triggers a retry:** - Connection timeout or network error - HTTP status codes other than 200 (including 201, 202, etc.) - Response time exceeds 5 seconds **What does NOT trigger a retry:** - HTTP 200 OK (even with error message in body - always return 200 for success) #### Handling Failures If all retry attempts fail, the webhook will be marked as failed and **will not be retried automatically**. You should: 1. Monitor your webhook endpoint health 2. Implement proper error logging 3. Contact Aslan support if you're experiencing delivery issues 4. Have a fallback mechanism to periodically check for new reports via the API #### Idempotency Webhooks may be delivered more than once due to: - Network issues during response - Retries after temporary failures - System failovers **Best Practice**: Use the `eventId` to deduplicate webhook events: ```javascript // Example: Track processed events in database async function processWebhookEvent(event) { const {eventId, eventType, body} = event; // Check if already processed const existing = await db.webhookEvents.findOne({eventId}); if (existing) { console.log(`Event ${eventId} already processed, skipping`); return; } // Process the event await processDeductionsReport(body); // Mark as processed await db.webhookEvents.insert({ eventId, eventType, processedAt: new Date() }); } ``` ### Testing Webhooks During integration: 1. **Request Test Events**: Contact Aslan support to trigger test webhook events 2. **Verify Signature**: Ensure your signature verification works correctly 3. **Test Error Scenarios**: Temporarily return non-200 status codes to verify retry behavior 4. **Check Idempotency**: Process the same event twice to ensure proper deduplication 5. **Monitor Response Time**: Ensure your endpoint responds within 5 seconds ### Best Practices 1. **Respond Quickly**: Return 200 OK within 5 seconds, then process asynchronously 2. **Verify Signatures**: Always validate the HMAC signature before processing 3. **Check Timestamps**: Reject webhooks with timestamps older than 5 minutes 4. **Handle Duplicates**: Use `eventId` for deduplication 5. **Log Everything**: Log all webhook receipts and processing outcomes 6. **Monitor Health**: Set up alerts for webhook delivery failures 7. **Use HTTPS**: Never accept webhooks over plain HTTP 8. **Implement Fallbacks**: Have a backup mechanism to check for reports if webhooks fail ### Troubleshooting **Webhooks not being received:** - Verify your webhook URL is configured correctly (contact Aslan support) - Ensure your endpoint is publicly accessible over HTTPS - Check firewall/security group rules aren't blocking Aslan's IP ranges **Signature verification failing:** - Ensure you're using the correct API Secret - Verify you're including the full JSON body in the signature string - Check the signature string format: `"POST\n{PATH}\n{TIMESTAMP}\n{JSON_BODY}\n"` - Make sure you're using the exact JSON body as received (don't re-serialize) **Retries exhausted:** - Check your endpoint response time (must be < 5 seconds) - Ensure you're returning HTTP 200 (not 201, 202, or other 2xx codes) - Review application logs for errors during webhook processing **Need Help?** Contact partner-support@aslan.io with: - Your webhook URL - The `eventId` of failed webhook(s) - Timestamp of the issue - Any error logs from your application ## Request & Response Format ### Request Format - **Content-Type:** `application/json` - **Character Encoding:** UTF-8 - **Date Format:** ISO 8601 (e.g., `2024-10-10` for dates, `2024-10-10T15:30:00Z` for timestamps) ### Response Format All successful responses return JSON with appropriate HTTP status codes: - `200 OK` - Request succeeded - `201 Created` - Resource successfully created - `400 Bad Request` - Invalid request data - `401 Unauthorized` - Authentication failed - `403 Forbidden` - Insufficient permissions - `404 Not Found` - Resource not found - `409 Conflict` - Resource already exists (e.g., duplicate email) - `429 Too Many Requests` - Rate limit exceeded - `500 Internal Server Error` - Server error ## Error Handling ### Error Response Structure All error responses follow a consistent structure: ```json { "timestamp": "2024-10-10T15:45:30Z", "status": 400, "error": "Bad Request", "message": "Validation failed", "path": "/api/v1/companies/{companyId}/users", "errors": [ { "field": "email", "message": "Email address is already in use", "rejectedValue": "john.doe@example.com" } ], "traceId": "a1b2c3d4-e5f6-4a8b-9c0d-e1f2a3b4c5d6" } ``` ### Error Fields | Field | Description | | --- | --- | | `timestamp` | When the error occurred (ISO 8601) | | `status` | HTTP status code | | `error` | HTTP status text | | `message` | Human-readable error description | | `path` | API endpoint where the error occurred | | `errors` | Detailed validation errors (for 400 responses) | | `traceId` | Unique identifier for debugging - **provide this to support** | ### Common Error Scenarios **401 Unauthorized** - Invalid or expired authentication credentials **404 Not Found** - Resource doesn't exist **409 Conflict** - Resource already exists (e.g., employee with duplicate email) **429 Too Many Requests** - Rate limit exceeded, retry after specified time ## Rate Limiting To ensure fair usage and system stability, the API implements rate limiting: - **Rate Limit:** 1000 requests per hour per API Key - **Burst Limit:** 100 requests per minute ### Rate Limit Headers Response headers include rate limit information: ``` X-RateLimit-Limit: 1000 X-RateLimit-Remaining: 987 X-RateLimit-Reset: 1697127056 ``` ### Handling Rate Limits When you exceed the rate limit, you'll receive a `429 Too Many Requests` response. Implement exponential backoff when receiving 429 responses. ## Support ### Getting Help For technical support, integration assistance, or to report issues: - **Email**: partner-support@aslan.io - **Documentation**: This comprehensive guide includes authentication, endpoint reference, and integration examples - **Response Time**: We aim to respond to all inquiries within 1 business day ### Reporting Issues When reporting an issue, please include the following information to help us resolve it quickly: 1. **Trace ID**: The `traceId` from the error response (this is critical for debugging) 2. **Request Details**: HTTP method, endpoint path, and timestamp 3. **Request Payload**: Sanitized request body (remove sensitive data like actual employee names/emails) 4. **Response Received**: Full error response including status code 5. **Environment**: Production or testing environment 6. **Integration Details**: Your programming language and library versions ### Best Practices for Integration - **Error Handling**: Always handle errors gracefully and implement retry logic with exponential backoff - **Logging**: Log the `traceId` from all requests for troubleshooting purposes - **Testing**: Test your integration thoroughly in a non-production environment before going live - **Security**: Never log or expose API secrets in your application code or logs - **Rate Limits**: Implement rate limit handling to avoid service disruptions ## Additional Resources - **API Reference**: Detailed endpoint documentation, schemas, and examples are available - **Change Log**: API changes and version updates will be communicated via email to registered partners - **Status Updates**: System status and planned maintenance notifications will be sent to partner contacts For onboarding assistance or partnership inquiries, please contact your Aslan account manager.