Integration Guide

Everything your developer needs to connect your software to ParcelFly.

If you run your own website, ERP or warehouse system, you do not have to type parcels into the ParcelFly dashboard by hand. An API key lets your software talk to ParcelFly directly — booking parcels the moment an order is placed, and reading their status back into your own screens.

Hand this page to whoever builds your software — or download it and give them the file. Everything below is exactly what they need; there is nothing else to sign up for.

Manage your keys

The Markdown file is plain text — keep it in your repository, or paste it into a coding assistant so it writes against the real contract instead of guessing. Save as PDF opens your browser’s print window; choose “Save as PDF” as the destination.

1. Create a key

On the API Access page, choose New API Key. Give it a name you will recognise, a lifetime, and tick only the things your software actually needs to do.

The key is shown once and never again. We store a scrambled copy to check it against, so nobody — including us — can read it back to you later. Copy it into your software or password manager before closing that window. If you lose it, revoke the key and make another.

2. Send it with every request

Put the key in the Authorization header, prefixed with Api-Key. Note that is not the word Bearer — a common first mistake.

Header
Authorization: Api-Key jcZXaCUL.weiqBn0jVd6qiVXx96SNYpCP33FR9MDk

The part before the dot is the key's prefix. It is the only half we show you on the keys page, so you can match a key in your config against one in your list without exposing the secret half.

Base URL
https://api.parcelflybd.com/merchant-api/v1

What a key is allowed to do

Each key carries a list of scopes. A call outside a key's scopes is refused even though the key itself is valid, so a read-only key used on your public website cannot delete anything if it is ever taken.

ScopeLets your softwareNeeded for
parcel.readList your parcels and their statusGET /parcels/
parcel.createBook a new parcelPOST /parcels/create/
parcel.updateCorrect a parcel it has already bookedPATCH /parcels/<id>/update/
parcel.deleteCancel a parcel it has already bookedDELETE /parcels/<id>/delete/

The calls you can make

List your parcels. Returns a page at a time, newest first, in the shape { count, next, previous, results }. You only ever see your own parcels — the key decides whose.

curl
curl "https://api.parcelflybd.com/merchant-api/v1/parcels/?page=1" \
  -H "Authorization: Api-Key YOUR_KEY_HERE"

Book a parcel. district and subdistrict are ID numbers, not names, and the subdistrict must belong to the district. Fetch the lists once from /core/districts/ and /core/districts/<id>/subdistricts/ and cache them — they rarely change.

curl
curl -X POST "https://api.parcelflybd.com/merchant-api/v1/parcels/create/" \
  -H "Authorization: Api-Key YOUR_KEY_HERE" \
  -H "Content-Type: application/json" \
  -d '{
    "sender_information": {
      "name": "My Shop",
      "phone_number": "01712345678",
      "address": {
        "district": 1,
        "subdistrict": 12,
        "postal_code": "1216",
        "street_address": "House 12, Road 4, Mirpur"
      }
    },
    "recipient_information": {
      "name": "Rahim Uddin",
      "phone_number": "01812345678",
      "address": {
        "district": 3,
        "subdistrict": 45,
        "postal_code": "4700",
        "street_address": "Holding 9, Ward 2"
      }
    },
    "weight": 1.5,
    "item_type": 2,
    "cod_amount": 1450,
    "delivery_mode": "regular",
    "invoice": "INV-2026-0041",
    "note": "Call before delivery"
  }'

Correct or cancel one.

Only while the parcel is still requested — that is, before a rider has been sent for it. Once we have collected it, the parcel belongs to the delivery network and the call is refused. This is the same rule the dashboard follows.

curl
# Change just the note, leaving everything else alone
curl -X PATCH "https://api.parcelflybd.com/merchant-api/v1/parcels/1042/update/" \
  -H "Authorization: Api-Key YOUR_KEY_HERE" \
  -H "Content-Type: application/json" \
  -d '{"note": "Customer asked for evening delivery"}'

# Cancel it
curl -X DELETE "https://api.parcelflybd.com/merchant-api/v1/parcels/1042/delete/" \
  -H "Authorization: Api-Key YOUR_KEY_HERE"

Reference lists: districts, areas and item types

A parcel does not carry the names of places — it carries their ID numbers. These three lists are how you turn a customer's address into the numbers the booking call expects, and they map an item type to its default delivery charge.

These need no API key. They are the same public lists the ParcelFly website uses, so you can fetch them from anywhere, including a build step. They change rarely — fetch once, cache, and refresh on a schedule rather than on every order.

ListCallReturns
DistrictsGET /core/districts/Every district, { id, name }
Areas in a districtGET /core/districts/<id>/subdistricts/{ id, name, zone, district }
All areasGET /core/subdistricts/Every subdistrict in the country, same shape
Item typesGET /parcel/allowed-item-types/{ id, item_type, default_delivery_charge }

All four return a plain array, not a page — there is no results wrapper and no page parameter on these.

curl
# Districts — no key needed
curl "https://api.parcelflybd.com/core/districts/"
# [{"id":1,"name":"Dhaka"},{"id":29,"name":"Bagerhat"}, ...]

# The areas inside one district
curl "https://api.parcelflybd.com/core/districts/1/subdistricts/"
# [{"id":4,"name":"Dhanmondi","zone":"Dhaka Metro South","district":1}, ...]

# What you may send, and what we charge to deliver it
curl "https://api.parcelflybd.com/parcel/allowed-item-types/"
# [{"id":2,"item_type":"Clothing","default_delivery_charge":60}, ...]

The subdistrict must belong to the district you send with it. Sending Dhanmondi's ID alongside Chattogram's district ID is refused with a 400 — we check the pair rather than trusting either alone. Always pick the district first, then choose from that district's own list.

default_delivery_charge is what a parcel of that type costs to deliver by default. Your own negotiated rate may differ, and the rate we actually apply is the one on the parcel we send back to you — read it from the create response rather than calculating it yourself from this list.

Node.js
// Build the two lookups your order form needs, once at startup.
const PUBLIC = "https://api.parcelflybd.com"

const [districts, itemTypes] = await Promise.all([
  fetch(`${PUBLIC}/core/districts/`).then((r) => r.json()),
  fetch(`${PUBLIC}/parcel/allowed-item-types/`).then((r) => r.json()),
])

// Areas are fetched per district, when the customer picks one.
async function areasIn(districtId) {
  const response = await fetch(`${PUBLIC}/core/districts/${districtId}/subdistricts/`)

  return response.json()
}

const dhaka = districts.find((d) => d.name === "Dhaka")
const clothing = itemTypes.find((t) => t.item_type === "Clothing")
// → use dhaka.id and clothing.id in the booking call

In your own language

Node.js
// Node 18+ — no dependencies needed
const BASE = "https://api.parcelflybd.com/merchant-api/v1"
const KEY = process.env.PARCELFLY_API_KEY   // never hard-code it

async function createParcel(parcel) {
  const response = await fetch(`${BASE}/parcels/create/`, {
    method: "POST",
    headers: {
      "Authorization": `Api-Key ${KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify(parcel),
  })

  if (!response.ok) {
    // 401 means the key is wrong, revoked or expired.
    // 403 means the key is fine but lacks the scope for this call.
    throw new Error(`ParcelFly ${response.status}: ${await response.text()}`)
  }

  return response.json()
}
Python
# Python 3.8+ — requests, the one dependency most projects already have
#   pip install requests
import os
import requests

BASE = "https://api.parcelflybd.com/merchant-api/v1"
KEY = os.environ["PARCELFLY_API_KEY"]   # never hard-code it

session = requests.Session()
session.headers.update({"Authorization": f"Api-Key {KEY}"})


class ParcelFlyError(RuntimeError):
    """Carries the status and the body — the body names the offending field."""


def _check(response):
    if not response.ok:
        # 401 the key is wrong, revoked or expired.
        # 403 the key is fine but lacks the scope for this call.
        raise ParcelFlyError(f"ParcelFly {response.status_code}: {response.text}")

    return response


def create_parcel(parcel: dict) -> dict:
    response = session.post(f"{BASE}/parcels/create/", json=parcel, timeout=30)

    return _check(response).json()


def iter_parcels(**filters):
    """Yields every parcel, following the pages so callers do not have to."""
    url = f"{BASE}/parcels/"
    params = {"page": 1, **filters}

    while url:
        page = _check(session.get(url, params=params, timeout=30)).json()
        yield from page["results"]
        # "next" is a full URL and already carries the query string.
        url, params = page.get("next"), None


parcel = create_parcel({
    "sender_information": {
        "name": "My Shop",
        "phone_number": "01712345678",
        "address": {
            "district": 1,
            "subdistrict": 12,
            "postal_code": "1216",
            "street_address": "House 12, Road 4, Mirpur",
        },
    },
    "recipient_information": {
        "name": "Rahim Uddin",
        "phone_number": "01812345678",
        "address": {
            "district": 3,
            "subdistrict": 45,
            "postal_code": "4700",
            "street_address": "Holding 9, Ward 2",
        },
    },
    "weight": 1.5,
    "item_type": 2,
    "cod_amount": 1450,
    "delivery_mode": "regular",
    "invoice": "INV-2026-0041",
})
print(parcel["id"])
PHP
<?php
// PHP 8 — cURL, no library needed
$base = "https://api.parcelflybd.com/merchant-api/v1";
$key  = getenv("PARCELFLY_API_KEY");   // never hard-code it

$ch = curl_init("$base/parcels/?page=1");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ["Authorization: Api-Key $key"],
]);

$body   = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($status !== 200) {
    throw new RuntimeException("ParcelFly $status: $body");
}

$parcels = json_decode($body, true)["results"];

When something is refused

CodeWhat it meansWhat to do
401The key is missing, mistyped, revoked or expiredCheck the header spells Api-Key, then check the key's state on the keys page
403The key is valid but lacks the scope for this callScopes cannot be edited after issue — create a new key with the scope you need and revoke the old one
404No such parcel, or it belongs to another merchantBoth look identical on purpose, so nobody can discover another merchant's parcel IDs by probing
400Something in the body is wrongThe response names the field and the reason — log it whole rather than just the status

Rules worth knowing before you build

  • Keys expire. Put a reminder in your calendar a week before the date shown on the keys page. An expired key fails with 401 at whatever hour it happens to lapse.
  • You may hold a limited number of active keys at once. The create window tells you how many you have left. Revoked and expired ones do not count against it.
  • A key name cannot be reused, even after that key is revoked. It keeps your history readable — two different keys never share a name.
  • Revoking takes effect immediately. There is no grace period, so deploy the new key before revoking the old one.
  • Keep the key out of your source code. Read it from an environment variable or your host's secret store. A key committed to a repository should be treated as leaked and revoked.
  • Never put a key in browser code. Anything running on a customer's device can be read. Calls to ParcelFly belong on your server.

Stuck on something this page does not answer? Your hub can put you in touch with our technical team — tell them the key prefix you are using, never the key itself.