Bullet Express Courier Integration with Voila

This document provides a step-by-step guide to integrating Bullet Express with Voila.

Last updated 2 days ago

Bullet Express Courier Integration with Voila

Bullet Express specialises in whisking pallets and freight across the United Kingdom with admirable gusto; this guide shows you how to wire it into the Voila API so your consignments glide from warehouse to wheels with minimal human button‑mashing.

By the end, you will be able to:

  • Authenticate Bullet Express against a Voila API User using:

    • Company,

    • Testing and

    • API Key

  • Fetch available Bullet Express services for your account

  • Create pallet and freight labels and download the PDF

  • Track consignments with full event history

  • Retrieve ETAs and proof of delivery (POD) images

  • Cancel consignments when required

What this integration does

This tutorial walks you through how to connect Bullet Express to Voila so that you can:

  • Discover which Bullet Express services and unit codes are available for your account

  • Create consignments using a recommended service_identifier or explicit service and unit_code

  • Track, get ETA information, download POD images, and cancel consignments programmatically

Everything here is designed to be called from your own system; Voila takes care of talking to Bullet Express; you just send tidy JSON and enjoy tidy labels.

Prerequisites

Before you start, make sure you have:

  • An active Voila account with access to API Accounts

  • A Bullet Express account (via Vigo)

  • Your Bullet Express API Key from your Vigo account

Once you have these, you are ready to move from “phoning the depot” to “orchestrating consignments via API”.

Step 1: Set up or access your Voila API Account

  1. Log in to Voila.

  2. From the left sidebar, go to API Accounts.

  3. Either:

    • Select an existing API User you will use for Bullet Express, or

    • Click Create API User and provide a name; for example bullet-express-production.

  4. Click Create, then note down:

    • API User Name; for example bullet-express-production

    • Account Key

    • API Token (create one if none exists)

You will use api-user and api-token in the request headers for all Bullet Express API calls.

Step 2: Register Bullet Express as a courier

In this step you bind your Bullet Express credentials to a Voila API User using three key fields: Company, Testing, and API Key.

  1. In the API Accounts page, locate your chosen API User.

  2. In the Registered Auth column, click the + icon.

  3. In the modal:

    • Choose Bullet Express from the courier dropdown.

    • Fill in the Bullet Express credentials:

      • Company Name: a friendly identifier; for example MyBulletAccount

      • Testing: select whether this auth should use the Bullet Express test environment (Yes) or live environment (No)

      • API Key: your Bullet Express / Vigo API key

  4. Click Save.

  5. To verify, click View in the Registered Auth column and confirm Bullet Express appears in the list, with your Company Name and Testing flag as expected.

> The Company Name you enter here is important: if you provide one, you must send it as auth_company in your API requests. If you did not set a Company Name, do not include auth_company.

> The Testing setting controls whether Voila talks to Bullet Express’ test or live environment for this API User; use Yes while integrating; use No for production.

Step 3: Get available Bullet Express shipping services

Before creating labels, you can fetch the available Bullet Express services for your account. This returns a list of service and unit_code combinations wrapped in a convenient service_identifier.

Endpoint

POST: https://app.heyvoila.io/api/couriers/v1/BulletExpress/get-services

Headers

  • api-user: your API User Name

  • api-token: your API Token

  • Content-Type: application/json

Example request body

{ 
  "auth_company": "MyBulletAccount" 
} 

If you did not set a Company Name when registering auth, omit the auth_company field entirely.

Example response

{
  "services": [
    {
      "service_courier": "BulletExpress",
      "service_identifier": "eyJzZXJ2aWNlIjoiTkQiLCJ1bml0X2NvZGUiOiJFUCJ9",
      "service_name": "Next Day - Euro Pallet (1 working days)",
      "service_price": null
    },
    {
      "service_courier": "BulletExpress",
      "service_identifier": "eyJzZXJ2aWNlIjoiRUMiLCJ1bml0X2NvZGUiOiJFUCJ9",
      "service_name": "Economy - Euro Pallet (3 working days)",
      "service_price": null
    }
  ]
}

Record the service_identifier for any services you intend to use. It is a base64‑encoded JSON object containing the service code and unit_code. You can pass it directly when creating labels, or decode it if you want to inspect the underlying values.

Step 4 (optional): Create Bullet Express service presets

Service presets let you save courier‑specific settings and reuse them simply by sending a dc_service_id in your API calls. This keeps your integration tidy, and makes it easy to switch services without changing your own code.

4.1 Create a Bullet Express preset

  1. In the Voila sidebar, go to Couriers → Service Presets.

  2. Search for Bullet Express and select it.

  3. Choose the API Account you set up earlier.

  4. Click Create Preset and fill in:

    • Name; for example Bullet Next Day Euro Pallet

    • DC Service ID; a unique ID; for example BULLET-ND-EP

    • Optional notes (for your own reference)

  5. In the courier‑specific section, configure:

    • service_identifier: base64‑encoded service from get-services (recommended)

    • service: service code; for example "ND"; alternative to service_identifier

    • unit_code: unit code; for example "EP"; alternative to service_identifier

    • surcharges: array of surcharge codes; for example ["AM", "LF"]

    • consignment_type: 1 (Collection), 2 (Delivery; default), 3 (Collection and Delivery)

    • item_type: default item type for parcels; for example "FULL" or "HALF"

    • is_residential: whether the delivery address is residential

    • customer_paperwork_required: whether paperwork is required at delivery

  6. Configure Transit Times (required): set minimum and maximum transit days per zone.

  7. Save the preset.

4.2 Example preset patterns

Some useful presets you might define:

  • Next Day Euro Pallet

    • dc_service_id: BULLET-ND-EP

    • service: "ND"

    • unit_code: "EP"

  • Economy with AM surcharge

    • dc_service_id: BULLET-EC-AM

    • service: "EC"

    • unit_code: "EP"

    • surcharges: ["AM"]

Presets are applied when you send dc_service_id in the shipment object; you can still override individual fields in the courier object.

Step 5: Create a Bullet Express shipping label

Once your API User and Bullet Express auth (Company Name, Testing Yes/No, API Key) are configured, you can start generating labels.

Endpoint

POST: https://app.heyvoila.io/api/couriers/v1/BulletExpress/create-label

Headers

  • api-user: your API User Name

  • api-token: your API Token

  • Content-Type: application/json

5.1 Request using service_identifier (recommended)

The easiest and most robust approach is to use the service_identifier you received from the get-services endpoint.

{
  "auth_company": "MyBulletAccount",
  "request_id": "unique-request-id",
  "shipment": {
    "collection_date": "2026-03-21",
    "reference": "REF123",
    "ship_from": {
      "company_name": "Warehouse Ltd",
      "address_1": "123 Industrial Estate",
      "postcode": "AB1 2CD",
      "country_iso": "GB",
      "phone": "0123456789"
    },
    "ship_to": {
      "company_name": "Customer Co",
      "address_1": "456 Business Park",
      "postcode": "XY9 8ZA",
      "country_iso": "GB",
      "phone": "09876543210",
      "email": "receiving@customer.co"
    },
    "parcels": [
      {
        "weight": 150,
        "quantity": 2
      }
    ],
    "courier": {
      "service_identifier": "eyJzZXJ2aWNlIjoiTkQiLCJ1bml0X2NvZGUiOiJFUCJ9"
    }
  }
}

service_identifier is a base64‑encoded JSON object containing the Bullet Express service and unit_code values for this consignment.

5.2 Request using service and unit_code directly

If you do not want to use service_identifier, you can send the service code and unit code explicitly and add surcharges and options as needed.

{
  "auth_company": "MyBulletAccount",
  "request_id": "unique-request-id",
  "shipment": {
    "collection_date": "2026-03-21",
    "reference": "REF123",
    "delivery_instructions": "Use rear loading bay",
    "ship_from": {
      "company_name": "Warehouse Ltd",
      "address_1": "123 Industrial Estate",
      "postcode": "AB1 2CD",
      "country_iso": "GB",
      "phone": "0123456789"
    },
    "ship_to": {
      "company_name": "Customer Co",
      "address_1": "456 Business Park",
      "postcode": "XY9 8ZA",
      "country_iso": "GB",
      "phone": "09876543210",
      "email": "receiving@customer.co"
    },
    "parcels": [
      {
        "weight": 150,
        "quantity": 2,
        "item_type": "FULL"
      }
    ],
    "courier": {
      "service": "ND",
      "unit_code": "EP",
      "surcharges": ["AM"],
      "consignment_type": 2,
      "delivery_time": "10:00",
      "is_residential": false,
      "customer_paperwork_required": false
    }
  }
}

5.3 Request using a preset (`dc_service_id`)

If you have created a preset, you can reference it directly using dc_service_id. The preset holds the service configuration so that your integration does not need to.

{
  "auth_company": "MyBulletAccount",
  "request_id": "unique-request-id",
  "shipment": {
    "dc_service_id": "BULLET-ND-EP",
    "collection_date": "2026-03-21",
    "reference": "REF123",
    "ship_from": {
      "company_name": "Warehouse Ltd",
      "address_1": "123 Industrial Estate",
      "postcode": "AB1 2CD",
      "country_iso": "GB",
      "phone": "0123456789"
    },
    "ship_to": {
      "company_name": "Customer Co",
      "address_1": "456 Business Park",
      "postcode": "XY9 8ZA",
      "country_iso": "GB",
      "phone": "09876543210"
    },
    "parcels": [
      {
        "weight": 150,
        "quantity": 2
      }
    ]
  }
}

Preset values are automatically applied; you can still override individual fields by including them in the courier object.

5.4 Successful response

A successful label creation returns something similar to:

{
  "type": "PDF",
  "request_id": "unique-request-id",
  "tracking_codes": ["BULLET123456"],
  "tracking_urls": [],
  "label_url": "https://s3.../label.pdf",
  "uri": "https://s3.../label.pdf",
  "courier_specifics": {
    "consignment_number": "CON12345",
    "warnings": []
  }
}

You can store the tracking_codes, consignment_number, and label_url in your own system so that users can see and download labels and track consignments.

Step 6: Track a consignment

Once a label has been created, you can retrieve tracking events for the consignment.

Endpoint

POST: https://app.heyvoila.io/api/couriers/v1/BulletExpress/track

Request body

{
  "auth_company": "MyBulletAccount",
  "tracking_code": "BULLET123456"
} 

Example response

{
  "tracking_code": "BULLET123456",
  "consignment_number": "CON12345",
  "customer_reference": "REF123",
  "events": [
    {
      "code": "DEL",
      "description": "Delivered",
      "timestamp": "2026-03-22T10:30:00",
      "item_number": 1,
      "pod_name": "J Smith"
    },
    {
      "code": "OFD",
      "description": "Out for delivery",
      "timestamp": "2026-03-22T07:15:00",
      "item_number": 1,
      "pod_name": null
    }
  ]
}

Step 7: Get ETA

You can fetch the estimated delivery window for a consignment.

Endpoint

POST: https://app.heyvoila.io/api/couriers/v1/BulletExpress/get-eta

Request body

{
  "auth_company": "MyBulletAccount",
  "tracking_code": "BULLET123456"
}

Example response

{
  "tracking_code": "BULLET123456",
  "consignment_number": "CON12345",
  "etas": [
    {
      "start_window": "2026-03-22T09:00:00",
      "end_window": "2026-03-22T12:00:00",
      "uploaded": "2026-03-22T06:00:00"
    }
  ]
}

Step 8: Get proof of delivery (POD)

You can retrieve POD images for a delivered consignment.

Endpoint

POST: https://app.heyvoila.io/api/couriers/v1/BulletExpress/get-pod

Request body

{
  "auth_company": "MyBulletAccount",
  "tracking_code": "BULLET123456"
}

Example response

{
  "tracking_code": "BULLET123456",
  "consignment_number": "CON12345",
  "images": [
    {
      "image_type": "POD",
      "uid": "img-uuid-123",
      "data": "base64-encoded-image-data...",
      "mime_type": "image/png",
      "filename": "pod_image.png",
      "captured": "2026-03-22T10:30:00",
      "uploaded": "2026-03-22T10:35:00"
    }
  ]
}

Step 9: Cancel a consignment

If a consignment has been created in error, you can request cancellation.

Endpoint

POST: https://app.heyvoila.io/api/couriers/v1/BulletExpress/cancel-label

Request body

{
  "auth_company": "MyBulletAccount",
  "tracking_code": "BULLET123456"
}

Example response

{
  "success": true
}

Request field reference (for implementers)

Top level fields

  • auth_company (string, conditional): Company Name used when registering Bullet Express. Omit this if you did not set a Company Name in auth.

  • request_id (string, optional): your unique request identifier.

  • dc_service_id (string, optional): ID of the service preset to apply.

shipment object

  • collection_date (date, required): YYYY-MM-DD; must be today or later.

  • delivery_date (date, optional): YYYY-MM-DD.

  • reference (string, optional): customer reference; maximum 16 characters.

  • reference_2 (string, optional): secondary reference; maximum 16 characters.

  • delivery_instructions (string, optional): maximum 96 characters; split across four lines for the carrier.

ship_from and ship_to objects

  • company_name or name (string, required): at least one must be provided; maximum 30 characters.

  • address_1 (string, required): maximum 30 characters.

  • address_2, address_3, address_4 (string, optional): maximum 30 characters each.

  • postcode (string, required): maximum 10 characters.

  • country_iso (string, required): ISO‑2 country code; for example "GB"; converted to ISO‑3 internally.

  • phone (string, optional): 9 to 16 characters.

  • mobile (string, optional): used for SMS notifications if provided.

  • email (string, optional): maximum 50 characters.

parcels array (required; minimum 1)

  • weight (number, required): weight in kilograms; minimum 0.1.

  • quantity (integer, optional): number of items; default 1.

  • item_type (string, optional): for example "FULL" or "HALF"; defaults to "FULL" or the courier‑level item_type.

Courier‑specific fields (`courier` object)

  • service_identifier (string, recommended): base64‑encoded service from get-services; contains both service and unit_code.

  • service (string, required if no service_identifier): service code; for example "ND" for Next Day.

  • unit_code (string, required if no service_identifier): unit code; for example "EP" for Euro Pallet; maximum 2 characters.

  • surcharges (array, optional): surcharge codes; for example ["AM", "LF"].

  • consignment_type (integer, optional): 1 = Collection; 2 = Delivery (default); 3 = Collection and Delivery.

  • consignment_number (string, optional): custom consignment reference; maximum 7 characters; falls back to reference then order_id.

  • delivery_time (string, optional): HH:MM format.

  • collection_time (string, optional): HH:MM format.

  • customer_paperwork_required (boolean, optional): whether paperwork is required at delivery; default false.

  • total_spaces (integer, optional): override calculated pallet spaces.

  • cubic_metres (number, optional): total cubic metres of the shipment.

  • item_type (string, optional): default item type for all parcels; for example "FULL"; default "FULL".

  • is_residential (boolean, optional): whether the delivery address is residential; default false.

  • delivery_notes (object, optional): { "Line1", "Line2", "Line3", "Line4" }; maximum 48 characters per line.

  • collection_notes (object, optional): same structure as delivery_notes.

Common errors and how to fix them

API User not authenticated

  • Check api-user and api-token headers are correct.

  • Confirm the API User is enabled.

not authenticated for this courier

  • Bullet Express credentials may be missing.

  • auth_company might not match the registered Company Name.

  • Confirm the Testing Yes/No flag on your auth matches the environment you expect.

service and unit_code required

  • Either provide service_identifier from get-services, or both service and unit_code explicitly.

collection_date must be today or later

  • Ensure collection_date is not in the past.

Unsupported country code

  • Bullet Express primarily supports UK deliveries; check country_iso is a valid ISO‑2 code such as "GB".

Address field too long

  • Address lines are capped at 30 characters; postcode at 10; delivery notes at 48 per line.

consignment_number max 7 characters

  • If you use a custom consignment number, keep it to seven characters or fewer.

For authentication issues, verify:

  1. api-user is your API User Name.

  2. api-token is a valid token from the Tokens table.

  3. Bullet Express courier credentials are registered and active, including API Key.

  4. auth_company exactly matches the Company Name (if used).

  5. The Testing Yes/No setting is correct for the environment you intend to hit.