Platform Docs Welcome. The Sail API provides on-demand
delivery services. Connect to multiple providers (Bolt, Uber, Yango) through a single,
unified API.
You're not signed in. Sign in via your invite to manage keys.
All API requests should be made to https://api.sailrides.co. All endpoints are
versioned and prefixed with /v1.
1 Create your API key Sign in to your Sail Platform account to manage your API key, or register your interest
to get started.
For security, load your API key from environment variables (for example via .env) and never store it in browser storage.
2 Connect delivery providers Connect the accounts you'll use when booking deliveries via the API. You can disconnect anytime.
3 Try a request Run a quick quote search using your SAIL_API_KEY environment variable.
Successful responses include header X-Sail-Status: Smooth Sailing.
The Sail API provides a comprehensive delivery platform that connects you to multiple providers through a single, unified interface.
Multi-Provider Support : Connect to Bolt, Uber, and Yango through one API Unified Interface : Consistent request/response format across all providers Real-time Pricing : Get accurate quotes and delivery estimates Provider Management : Easily connect and disconnect provider accounts Status Tracking : Monitor delivery progress with comprehensive status updates The Sail API acts as a unified layer on top of multiple delivery providers, abstracting away the differences between their individual APIs. This allows you to integrate once and get access to multiple providers seamlessly.
All API requests should be made to https://api.sailrides.co. All endpoints are versioned and prefixed with /v1.
https://api.sailrides.co/v1/deliveries/quotes
https://api.sailrides.co/v1/deliveries
https://api.sailrides.co/v1/deliveries/{'{deliveryId}'} All API requests must be authenticated using your API key in the Authorization header:
Authorization: ApiKey sail_prod_compass_abc123... To get your API key:
Sign in to your Sail Platform account Click “Create API Key” in the Getting Started section Copy your key immediately - it won’t be shown again for security reasons Store it securely in your environment variables Store your API key in environment variables, not in code Never commit API keys to version control Rotate your API key if you suspect it’s been compromised Use HTTPS for all API requests API requests are rate limited per API key to ensure fair usage:
Quotes : 100 requests per minute Deliveries : 50 requests per minute Status Checks : 200 requests per minute Rate limit headers are included in all responses:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 99
X-RateLimit-Reset: 1695123456 When you receive a 429 Too Many Requests response:
Check the X-RateLimit-Reset header for when the limit resets Implement exponential backoff for retries Consider webhook notifications instead of polling for high-frequency updates Get delivery quotes from all connected providers for a given pickup and destination.
POST /v1/deliveries/quotes
{
"pickup": {
"lat": 5.6037,
"lng": -0.1870,
"address": "Optional pickup address"
},
"destination": {
"lat": 5.5600,
"lng": -0.2050,
"address": "Optional destination address"
}
} pickup.lat , pickup.lng : Pickup coordinates (decimal degrees) destination.lat , destination.lng : Destination coordinates (decimal degrees) pickup.address , destination.address : Human-readable addresses for reference {
"quotes": [
{
"id": "quote_1234567890",
"provider": "bolt",
"service_name": "Bolt",
"price": {
"amount": 12.5,
"currency": "GHS"
},
"eta_minutes": 5,
"expires_at": "2024-01-15T14:10:00Z"
}
],
"meta": {
"total_providers": 3,
"successful_providers": 2,
"warnings": []
}
} quotes.id : Unique quote identifier for booking quotes.provider : Provider name (bolt, uber, yango) quotes.service_name : Display name of the service quotes.price.amount : Total price in local currency quotes.price.currency : 3-letter currency code quotes.eta_minutes : Estimated time to pickup in minutes quotes.expires_at : Quote expiration time (ISO 8601) Common Error Responses:
{
"error": {
"code": "invalid_location",
"message": "Pickup or destination location is invalid",
"details": {
"field": "pickup",
"reason": "coordinates_outside_service_area"
}
}
} invalid_location: Coordinates outside service area no_providers_available: No providers servicing the route rate_limited: Too many quote requests invalid_coordinates: Malformed coordinate data Manage delivery lifecycle from booking to completion.
Book a delivery using a previously obtained quote ID.
POST /v1/deliveries
{
"quoteId": "quote_1234567890",
"webhookUrl": "https://your-app.com/webhooks/sail",
"customerInfo": {
"name": "John Doe",
"phone": "+233123456789"
},
"notes": "Please call upon arrival"
} quoteId : Valid quote ID (must not be expired) webhookUrl : URL to receive status updates customerInfo : Customer contact information notes : Additional delivery instructions {
"id": "delivery_1234567890",
"status": "pending",
"provider": "bolt",
"quoteId": "quote_1234567890",
"fare": 12.5,
"currency": "GHS",
"createdAt": "2024-01-15T14:05:00Z",
"pickup": {
"address": "Accra Mall, Accra",
"coordinates": { "lat": 5.6037, "lng": -0.1870 }
},
"destination": {
"address": "Kotoka International Airport",
"coordinates": { "lat": 5.5600, "lng": -0.2050 }
}
} Retrieve delivery status and details.
GET /v1/deliveries/{deliveryId}
{
"id": "delivery_1234567890",
"status": "completed",
"provider": "bolt",
"quoteId": "quote_1234567890",
"fare": 12.5,
"currency": "GHS",
"createdAt": "2024-01-15T14:05:00Z",
"updatedAt": "2024-01-15T14:45:00Z",
"driver": {
"name": "Kwame Asante",
"phone": "+233987654321",
"vehicle": "Toyota Corolla",
"plateNumber": "GT-1234-AB"
},
"trackingUrl": "https://bolt.eu/track/abc123"
} Deliveries progress through these statuses:
pending - Awaiting provider acceptance accepted - Driver assigned, en route to pickup arriving - Driver arriving at pickup location in_progress - Package in transit, en route to destination completed - Delivery completed successfully cancelled - Delivery cancelled by user or provider failed - Delivery failed (no-show, payment issue, etc.) Cancel an active delivery.
POST /v1/deliveries/{deliveryId}/cancel
Note: Only deliveries in pending or accepted status can be cancelled.
{
"reason": "customer_request",
"refundRequested": true
} customer_request: Customer requested cancellation provider_unavailable: Provider unable to complete payment_failed: Payment processing failed Status polling is an alternative to webhooks for monitoring delivery progress. Poll delivery status at regular intervals to track updates.
Use polling when:
Your application cannot receive webhook requests You need real-time status updates for specific deliveries You want to supplement webhook notifications You’re building a simple application without webhook infrastructure Use webhooks when:
You need to monitor many deliveries simultaneously You want instant notifications You want to reduce API calls You have server infrastructure to receive webhooks Recommended polling frequencies based on delivery status:
Active statuses (pending, accepted, arriving): Every 30-60 seconds In-progress : Every 60-120 seconds Final statuses (completed, cancelled, failed): Stop polling Rate Limiting Considerations:
Respect the 50 requests/minute limit for delivery endpoints Implement backoff for failed requests Use exponential backoff for rate-limited responses Set reasonable timeouts - Don’t wait indefinitely for responses Handle rate limits gracefully - Implement exponential backoff Store delivery state - Avoid re-polling completed deliveries Use conditional requests - Check Last-Modified headers if available Monitor API usage - Track your request count to avoid hitting limits Have a fallback - Consider switching to webhooks if polling becomes inefficient Webhooks provide real-time notifications when delivery status changes occur. Instead of constantly polling our API, you can receive immediate updates by configuring webhook endpoints.
Configure your webhook URL when creating a delivery by including the webhookUrl field in your request. The webhook URL will receive real-time notifications for that delivery.
Webhook URL Requirements:
Must be publicly accessible Should respond within 5 seconds Must return a 200 status code Supports HTTPS endpoints only The following events are sent via webhooks:
Event Description When Sent delivery.createdNew delivery created Immediately after creation delivery.status_updatedDelivery status changed When status changes delivery.completedDelivery completed successfully When delivery reaches completed status delivery.cancelledDelivery was cancelled When delivery is cancelled delivery.failedDelivery failed When delivery fails
All webhook payloads contain this structure:
{
"id": "webhook_evt_1234567890",
"event": "delivery.status_updated",
"created_at": "2024-01-15T10:30:00Z",
"data": {
"delivery_id": "delivery_1234567890",
"status": "in_progress",
"previous_status": "accepted",
"updated_at": "2024-01-15T10:30:00Z"
}
} Payload Fields:
id: Unique webhook event identifier event: Type of event that occurred created_at: Timestamp when webhook was sent data: Event-specific data All webhook requests include security headers:
Headers:
X-Sail-Signature: HMAC-SHA256 signature of the payload X-Sail-Timestamp: Unix timestamp of when webhook was sent Signature Verification: The signature is calculated as: HMAC-SHA256(secret, payload)
Security Best Practices:
Always verify webhook signatures Reject requests older than 5 minutes Use HTTPS for webhook endpoints Keep your webhook secret secure Implement rate limiting Reliability Features:
Automatic retries for failed deliveries Exponential backoff retry strategy Maximum of 5 retry attempts 5-second timeout per attempt Retry Schedule:
Attempt 1: Immediate Attempt 2: 30 seconds later Attempt 3: 2 minutes later Attempt 4: 10 minutes later Attempt 5: 30 minutes later If all retries fail, the webhook is marked as failed and you can view failed attempts in your dashboard.
Use these tools to test webhooks during development:
ngrok - Expose local endpoints to the internet webhook.site - Temporary webhook URL for testing RequestBin - Inspect webhook requests Local testing - Use curl to simulate webhook calls # Test webhook locally with curl
curl -X POST http://localhost:3000/webhooks/sail \
-H "Content-Type: application/json" \
-H "X-Sail-Signature: sha256=test_signature" \
-H "X-Sail-Timestamp: $(date +%s)" \
-d '{"event":"delivery.status_updated","data":{"status":"completed"}}' Respond quickly - Return within 5 seconds Process asynchronously - Don’t block webhook delivery Idempotency - Handle duplicate webhook deliveries Retry strategy - Implement your own retry logic for critical failures Logging - Log all webhook events for debugging Have fallback - Use polling if webhooks fail repeatedly The API uses conventional HTTP status codes and returns detailed error information in JSON format to help you troubleshoot issues.
200 OK : Request successful 201 Created : Resource created successfully 400 Bad Request : Invalid JSON or missing required fields 401 Unauthorized : Invalid or missing API key 403 Forbidden : Insufficient permissions 404 Not Found : Invalid delivery ID or endpoint 409 Conflict : Resource state conflict 422 Unprocessable Entity : Valid JSON but invalid data 429 Rate Limited : Too many requests 500 Internal Server Error : Unexpected server error 502 Bad Gateway : Upstream provider error 503 Service Unavailable : Temporary service outage Authentication Errors invalid_api_key (401): API key is invalid or revoked missing_api_key (401): API key not provided in header expired_api_key (401): API key has expired Request Validation Errors invalid_json (400): Request body is not valid JSON missing_required_field (422): Required field is missing invalid_field_value (422): Field value is invalid field_too_long (422): Field value exceeds maximum length Quote Errors invalid_quote (400): Quote ID is invalid or expired quote_not_found (404): Quote does not exist quote_expired (400): Quote has expired quote_used (400): Quote has already been used Delivery Errors delivery_not_found (404): Delivery does not exist delivery_not_cancellable (400): Delivery cannot be cancelled provider_unavailable (502): Provider service is down insufficient_funds (400): Insufficient funds for delivery Rate Limiting Errors rate_limit_exceeded (429): Rate limit exceeded daily_limit_exceeded (429): Daily rate limit exceeded Error Handling Best Practices 1. Always Check HTTP Status const response = await fetch(url, options);
if (!response.ok) {
const error = await response.json();
handleApiError(error);
return;
}
const data = await response.json(); 2. Handle Rate Limits Gracefully if (response.status === 429) {
const resetTime = parseInt(response.headers.get('X-RateLimit-Reset'));
const waitTime = resetTime - Math.floor(Date.now() / 1000);
if (waitTime > 0) {
await new Promise(resolve => setTimeout(resolve, waitTime * 1000));
// Retry the request
}
} 3. Use Request IDs for Debugging const requestId = response.headers.get('X-Request-ID');
console.log(`Request ${requestId} failed:`, error); 4. Implement Exponential Backoff async function makeRequestWithRetry(url, options, maxRetries = 3) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(url, options);
if (response.ok || response.status < 500) {
return response; // Success or client error - don't retry
}
if (attempt === maxRetries) break;
// Exponential backoff
const delay = Math.min(1000 * Math.pow(2, attempt), 30000);
await new Promise(resolve => setTimeout(resolve, delay));
} catch (error) {
if (attempt === maxRetries) throw error;
const delay = Math.min(1000 * Math.pow(2, attempt), 30000);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw new Error('Max retries exceeded');
} 5. Log Errors Appropriately function logApiError(error, context = {}) {
const logLevel = getLogLevel(error.code);
console[logLevel]('API Error:', {
code: error.code,
message: error.message,
requestId: context.requestId,
timestamp: new Date().toISOString(),
...context
});
}
function getLogLevel(errorCode) {
const clientErrors = ['invalid_api_key', 'missing_required_field', 'invalid_quote'];
const serverErrors = ['internal_error', 'provider_unavailable'];
if (clientErrors.includes(errorCode)) return 'warn';
if (serverErrors.includes(errorCode)) return 'error';
return 'info';
} Common Issues and Solutions 401 Unauthorized : Verify API key in Authorization header format: ApiKey your_key 429 Rate Limited : Implement rate limiting and exponential backoff Quote expired : Request a new quote before creating delivery Delivery not found : Verify delivery ID and account permissions Provider unavailable : Try again later or use different provider