Introduction

The EzyRunners Enterprise API lets you create deliveries, track orders in real-time, and receive webhook notifications — all authenticated with a single API key you generate from your dashboard.

API Version: v1  |  Protocol: HTTPS/HTTP  |  Format: JSON

Authentication

Every request must include your API key in the X-API-Key header. You can generate or rotate your key on the Settings → Integration page.

X-API-Key: YOUR_API_KEY
Content-Type: application/json
Important: The header is X-API-Key, not Authorization: Bearer.

Base URL

Client applications send requests to the EzyRunners server. All endpoints are prefixed with /api/v1/integrations.

EnvironmentEzyRunners Base URL
Production https://ezyrunners.com/api/v1/integrations

Example full endpoint: https://ezyrunners.com/api/v1/integrations/delivery/create

Your server URL vs EzyRunners URL: The base URL above is the EzyRunners server — your app calls these endpoints to create deliveries. The Webhook URL in your settings is where EzyRunners calls your server to send delivery status updates.

Rate Limits

Limits are enforced per API key per minute. When exceeded, the API returns 429 Too Many Requests.

PlanRequests / Minute
Starter60
Business300
Enterprise10,000

Contact us to upgrade your plan if you need higher limits.

Wallet & Billing

EzyRunners uses a prepaid wallet system. You add funds to your enterprise wallet through the dashboard, and every delivery automatically deducts the delivery fee from your balance at the moment the order is created.

How it works — step by step
  1. Top up: Add funds to your wallet via the Wallet page (bank transfer / UPI).
  2. Place order: When you call POST /delivery/create with "payment_method": "prepaid", the system calculates the delivery fee and immediately deducts it from your wallet.
  3. Confirmation in response: The create-delivery response includes wallet_deducted: true, delivery_fee (exact amount charged), and wallet_balance (remaining balance after deduction).
  4. Cancellation refund: If you cancel a delivery that is still pending or assigned (runner not yet picked up), the full delivery fee is automatically refunded to your wallet. The cancel response includes refund_amount.
  5. No refund after pickup: If the runner has already picked up the package (picked_up / in_transit / delivered), cancellation is not allowed.
  6. Insufficient balance: If your wallet balance is less than the delivery fee, the API returns 402 Payment Required with the required, available, and shortfall amounts.
Insufficient balance error example
HTTP 402 Payment Required

{
  "success": false,
  "error": "insufficient_balance",
  "message": "Insufficient wallet balance",
  "required": 85.00,
  "available": 30.00,
  "shortfall": 55.00
}
COD orders: If payment_method is cod, the delivery fee is still deducted from your wallet upfront. The customer pays the cod_amount in cash to the runner, who submits it separately.

Idempotency

To prevent duplicate deliveries on network retries, pass a unique X-Idempotency-Key header or include an order_id field. If the same key is seen within 24 hours, the original response is replayed without creating a new delivery or deducting the wallet again.

X-API-Key: YOUR_API_KEY
X-Idempotency-Key: ORDER-20260330-0042
Content-Type: application/json

Error Codes

HTTPerror fieldMeaning
400missing_required_fieldA required field is absent
400delivery_not_cancellableDelivery status doesn't allow cancellation
401missing_api_keyX-API-Key header was not sent
401invalid_api_keyKey not found or inactive
401expired_api_keyKey has passed its expiry date
402insufficient_balanceWallet balance too low
403account_suspendedEnterprise account suspended
403forbiddenDelivery belongs to a different enterprise
404not_foundDelivery ID not found
409duplicate_order_idorder_id was already used
422invalid_coordinatesLatitude/longitude out of range
429rate_limit_exceededToo many requests this minute
500internal_errorServer-side error

Create Delivery

POST /delivery/create

Creates a new delivery. The delivery fee is immediately deducted from your wallet (prepaid) or collected from the customer (cod). Supports idempotency via X-Idempotency-Key header or order_id field.

Request Body
{
  "order_id": "YOUR-ORDER-123",          // optional, for idempotency & dedup
  "pickup_location": {
    "address": "123 Main Street, Hyderabad",
    "latitude": 17.4239,
    "longitude": 78.4738,
    "contact_name": "Store Manager",
    "contact_phone": "+919876543210",
    "instructions": "Ask for order #123"
  },
  "drop_location": {
    "address": "456 Customer Lane, Hyderabad",
    "latitude": 17.4456,
    "longitude": 78.4892,
    "contact_name": "John Doe",
    "contact_phone": "+919876543211",
    "instructions": "Leave at door"
  },
  "package_details": {
    "type": "general",             // general | medicine | food | documents
    "weight_kg": 2.5,
    "is_fragile": false,
    "requires_cold_storage": false,
    "description": "Medicine package"
  },
  "vehicle_type": "two_wheeler",       // two_wheeler (default) | bike | bike_plus | auto | mini_truck | truck
  "payment_method": "prepaid",         // prepaid (wallet) | cod
  "order_value": 450.00,              // value of goods (for COD)
  "cod_amount": 0,                   // cash to collect from customer (if cod)
  "priority": "normal",              // normal | express (1.5x) | urgent (2x)
  "notes": "Handle with care"
}
Response 201 Created
{
  "success": true,
  "delivery_id": "EZY20260330AB1234",
  "internal_id": "65a1b2c3d4e5f6789abc1234",   // MongoDB internal ID
  "status": "pending",
  "tracking_url": "/track/EZY20260330AB1234",
  "delivery_fee": 85.00,
  "wallet_deducted": 85.00,          // amount deducted from wallet (same as delivery_fee)
  "wallet_balance": 9845.00,        // remaining wallet balance after deduction (INR)
  "estimated_pickup_time": "5-15 minutes",
  "message": "Delivery created. Finding runner..."
}
Wallet deduction: The delivery_fee is deducted from your wallet the instant this endpoint succeeds. If your balance is insufficient, the API returns 402 and no delivery is created.

Get Delivery Status

GET /delivery/{delivery_id}/status

Retrieve full details and live status of a delivery. delivery_id can be either the EzyRunners delivery ID (EZY…) or your own order_id.

Response
{
  "success": true,
  "delivery_id": "EZY20260330AB1234",
  "order_id": "YOUR-ORDER-123",
  "status": "in_transit",              // pending | assigned | picked_up | in_transit | delivered | cancelled | failed
  "pickup": { "address": "...", "latitude": 17.4239, "longitude": 78.4738 },
  "drop":   { "address": "...", "latitude": 17.4456, "longitude": 78.4892 },
  "delivery_fee": 85.00,
  "distance_km": 4.3,
  "tracking_url": "/track/EZY20260330AB1234",
  "runner_details": {                   // present when a runner is assigned
    "name": "Rahul K",
    "phone": "+91987654****"
  },
  "current_location": {               // present when runner is en route
    "latitude": 17.4350,
    "longitude": 78.4820,
    "updated_at": "2026-03-30T14:45:00Z"
  },
  "estimated_arrival": "8 minutes",
  "distance_remaining_km": 1.5,
  "timestamps": {
    "created_at": "2026-03-30T14:00:00Z",
    "assigned_at": "2026-03-30T14:05:00Z",
    "picked_at": "2026-03-30T14:25:00Z",
    "delivered_at": null
  },
  "proof_of_delivery": {              // present only when status = delivered
    "photos": ["..."],
    "signature": null,
    "notes": "Left with security"
  }
}

Cancel Delivery

POST /delivery/{delivery_id}/cancel

Cancel a delivery. Only deliveries in pending or assigned status can be cancelled. Once a runner has picked up the package, cancellation is not allowed.

Request Body
{
  "reason": "customer_request",     // customer_request | store_unavailable | item_unavailable | payment_failed | other
  "notes": "Customer changed their mind",
  "refund_delivery_fee": true       // default true — refunds delivery fee to wallet
}
Response
{
  "success": true,
  "delivery_id": "EZY20260330AB1234",
  "status": "cancelled",
  "refund_amount": 85.00,            // amount refunded to wallet (0 if refund_delivery_fee=false)
  "message": "Delivery cancelled successfully"
}
Automatic wallet refund: When refund_delivery_fee is true (default), the full delivery fee is instantly credited back to your wallet.

Tracking URL

GET /delivery/{delivery_id}/tracking-url

Get shareable tracking links and an embeddable map URL for a delivery.

Response
{
  "success": true,
  "tracking_url": "https://ezyrunners.com/track/EZY20260330AB1234",
  "embed_url": "https://ezyrunners.com/track/EZY20260330AB1234?embed=true",
  "qr_code_url": "https://ezyrunners.com/api/v1/integrations/delivery/EZY20260330AB1234/qr"
}

Bulk Create Deliveries

POST /delivery/bulk-create

Create up to 50 deliveries in a single API call. Each delivery in the array uses the same format as the single create endpoint. Wallet fee is deducted for each successful delivery individually.

Request Body
{
  "deliveries": [
    {
      "order_id": "ORD-001",
      "pickup_location": { "..." },
      "drop_location": { "..." },
      "package_details": { "..." },
      "payment_method": "prepaid"
    },
    { "..." }
  ]
}
Response
{
  "success": true,
  "total": 3,
  "created": 2,
  "failed": 1,
  "results": [
    { "index": 0, "success": true, "delivery_id": "EZY20260330AB1234", "status": "pending" },
    { "index": 1, "success": true, "delivery_id": "EZY20260330CD5678", "status": "pending" },
    { "index": 2, "success": false, "error": "insufficient_balance" }
  ]
}

Available Runners

GET /available-runners?latitude=17.4239&longitude=78.4738&radius_km=5

Check how many online runners are available near a pickup point before placing an order.

Query Parameters
ParameterTypeRequiredDescription
latitudefloatYesPickup latitude
longitudefloatYesPickup longitude
radius_kmfloatNoSearch radius in km (default: 5)
Response
{
  "success": true,
  "available_runners": [
    {
      "runner_id": "65a1b2c3...",
      "name": "Rahul K",
      "distance_km": 1.2,
      "vehicle_type": "bike",
      "rating": 4.8,
      "eta_minutes": 4
    }
  ],
  "total_available": 7,
  "search_radius_km": 5
}

Get Price Estimate

POST /estimate

Calculate the delivery fee before placing an order. No wallet deduction — this is a dry run only.

Request Body
{
  "pickup_latitude": 17.4239,
  "pickup_longitude": 78.4738,
  "drop_latitude": 17.4456,
  "drop_longitude": 78.4892,
  "weight_kg": 2.5,
  "vehicle_type": "two_wheeler",  // two_wheeler (default) | bike | bike_plus | auto | mini_truck | truck
  "package_type": "general",
  "priority": "normal"           // normal | express | urgent
}
Response
{
  "success": true,
  "estimate": {
    "distance_km": 4.30,
    "estimated_time_minutes": 10,
    "vehicle_type": "bike",
    "pricing": {
      "base_fare": 19.00,
      "distance_fare": 33.00,
      "weight_surcharge": 0,
      "vehicle_charge": 0,
      "platform_fee": 9.36,         // 18% of subtotal
      "total_fare": 61.36,
      "currency": "INR"
    },
    "priority_multiplier": 1.0,
    "estimated_pickup_time": "5-10 minutes"
  }
}

Pricing Rules

GET /pricing

Fetch the live pricing configuration to display fees to your customers or build your own fee calculator.

Pricing Formula
delivery_fee = (base_fare
               + max(0, distance_km - 1) × per_km_rate
               + weight_surcharge
               + vehicle_charge) × priority_multiplier
               + platform_fee (18%)
Surcharges
SurchargeValue
Night delivery (22:00–06:00)1.25× multiplier
Fragile handling+₹15 flat
Cold storage required+₹25 flat
Express priority1.5× multiplier
Urgent priority2.0× multiplier

Register Webhook

POST /webhook/register

Register a URL on your server where EzyRunners will POST real-time delivery events. You can register one webhook URL per enterprise account.

Request Body
{
  "url": "https://yourapp.com/webhooks/ezyrunners",
  "events": ["delivery.assigned", "delivery.picked_up", "delivery.delivered"],
  "secret": "your_custom_secret"     // optional — auto-generated if omitted
}
Response 201 Created
{
  "success": true,
  "data": {
    "webhook_id": "65a1b2c3d4e5f6789",
    "url": "https://yourapp.com/webhooks/ezyrunners",
    "events": ["delivery.assigned", "delivery.picked_up", "delivery.delivered"],
    "secret": "whsec_abc123xyz..."    // save this to verify incoming webhooks
  }
}

Webhook Event Types

EzyRunners will POST a JSON payload to your URL for each subscribed event.

Available events
EventFired when
delivery.createdA new delivery is created via API
delivery.assignedA runner accepts the delivery
delivery.picked_upRunner picks up the package
delivery.in_transitRunner is heading to the drop point
delivery.deliveredPackage delivered successfully
delivery.cancelledDelivery cancelled (with refund amount)
delivery.failedDelivery could not be completed
runner.location_updateRunner GPS location changed
runner.eta_updateEstimated arrival time changed
Example payload
{
  "event": "delivery.delivered",
  "event_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "delivery_id": "EZY20260330AB1234",
  "status": "delivered",
  "timestamp": 1743340920,            // Unix epoch (integer, not ISO string)
  "data": {
    "delivery_id": "EZY20260330AB1234",
    "status": "delivered",
    "pickup_address": "123 Main Street, Hyderabad",
    "drop_address": "456 Customer Lane, Hyderabad",
    "runner_name": "Rahul K",
    "runner_phone": "+91987654****",
    "delivery_fee": 85.00,
    "distance_km": 4.3
  }
}

Webhook Security (HMAC)

Every webhook POST includes an X-EzyRunners-Signature header. Use it to verify the request genuinely came from EzyRunners.

EzyRunners signs requests using the HMAC-SHA256 of "{timestamp}.{raw_payload}". The timestamp comes from the X-EzyRunners-Timestamp header (Unix integer as a string).

# Python verification example
import hmac, hashlib

def verify_webhook(request, secret):
    received_sig = request.headers.get('X-EzyRunners-Signature')  # "sha256=..."
    timestamp    = request.headers.get('X-EzyRunners-Timestamp')   # Unix int as string
    raw_body     = request.get_data()                                 # raw bytes unchanged

    # Message is: "{timestamp}.{raw_body}"
    message  = f"{timestamp}.{raw_body.decode('utf-8')}"
    expected = "sha256=" + hmac.new(
        secret.encode('utf-8'),
        message.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, received_sig)
Always use hmac.compare_digest (constant-time comparison) to prevent timing attacks. Never compare signatures with ==.

Webhook Logs

GET /webhook/logs?page=1&limit=20&event=delivery.delivered&status=failed

Retrieve the history of webhook deliveries, including failed attempts with error details.

Query Parameters
ParameterTypeDescription
pageintPage number (default: 1)
limitintResults per page, max 100 (default: 20)
eventstringFilter by event type
statusstringsuccess or failed

Test Webhook

POST /webhook/test

Send a signed test event to the webhook URL currently saved in Settings → Integration. No request body is needed. Use this to confirm your server is reachable and your HMAC verification logic is correct before going live.

Request
POST /api/v1/integrations/webhook/test
X-API-Key: YOUR_API_KEY

// No request body required.
// EzyRunners sends a signed test payload to your configured webhook URL.
Response
{
  "success": true,
  "message": "Test webhook delivered successfully",
  "status_code": 200,
  "response_time_ms": 145
}

Ready to integrate?

Generate your API key from the Settings → Integration page and start creating deliveries in minutes.