click-and-drop-api
This is an inofficial Python client for the Royal Mail Click & Drop API. The Click & Drop API allows you to import your orders, retrieve your orders and generate labels.
Read about Royal Mail's Click & Drop API:
Links for this package:
Installation
API Reference
This package has extensive documentation for the API Reference, which can be found here.
API Key
You need an API key to use the Click & Drop API. You can get this key here:
- Register an account at parcel.royalmail.com
- Login at auth.parcel.royalmail.com
- Go to
Settings→Integrations→Add a new integration→Click & Drop API - Fill out the details and save/update them.
- Copy the
Click & Drop API authorisation key
The API key will be used on the examples below to authenticate the requests.
OBA
Some endpoints like generating labels require an OBA (Online Business Account). Label generation is available for OBA accounts only:
- Private accounts pay as the label is generated.
- OBA accounts pay monthly via invoice.
This only OBA can generate labels on request.
- Apply for a Royal Mail Business Account (OBA).
- Link your OBA to Click & Drop.
- Fill in required account information and make sure you follow the law.
Examples
This sections guides you through some examples.
Export the API_KEY of your account to run the examples.
The examples use the simple API that is based on the generated API and reduces the amount of code.
Retrieve the version
Retrieving the version is useful to understand if you can use the API without authentication.
#!/usr/bin/env python
from click_and_drop_api.simple import ClickAndDrop
import os
# navigate to https://business.parcel.royalmail.com/settings/channels/
# Configure API key authorization: Bearer
API_KEY = os.environ["API_KEY"]
api = ClickAndDrop(API_KEY)
version = api.get_version()
# https://api.parcel.royalmail.com/#tag/Version
print("commit:", version.commit)
print("build:", version.build)
print("release:", version.release)
print("release date:", version.release_date)
Output:
commit: 9236a82ff38de629d7b987a4d291d81c34fdd531
build: 20260107.6
release: Release-20260107.3
release date: 2026-01-22 10:20:54+00:00
View specific orders
The image below shows orders that were created as examples.

The script below retrieves information about these orders, by id (int) and by reference (str).
#!/usr/bin/env python
from click_and_drop_api.simple import ClickAndDrop
import os
# navigate to https://business.parcel.royalmail.com/settings/channels/
# Configure API key authorization: Bearer
API_KEY = os.environ["API_KEY"]
api = ClickAndDrop(API_KEY)
example_order_id = 1002
example_order_reference = "my-ref-9999"
orders = api.get_orders([example_order_id, example_order_reference])
for order in orders:
print("Order Identifier:", order.order_identifier)
print("Order Reference:", order.order_reference)
print("Order Date:", order.order_date)
print("Order Printed On:", order.printed_on)
print("Order Manifested On:", order.manifested_on)
print("Order Shipped On:", order.shipped_on)
for package in order.packages:
print("\tPackage Number:", package.package_number)
print("\tPackage Tracking Number:", package.tracking_number)
Output:
Order Identifier: 1002
Order Reference: example-order-20260213211828
Order Date: 2026-02-13 21:18:28.936246
Order Printed On: None
Order Manifested On: None
Order Shipped On: None
Package Number: 1
Package Tracking Number: None
Order Identifier: 1003
Order Reference: my-ref-9999
Order Date: 2026-02-09 20:09:19.587738
Order Printed On: None
Order Manifested On: None
Order Shipped On: None
Package Number: 1
Package Tracking Number: None
View a single order
#!/usr/bin/env python
"""Print details of a single order by order number or reference.
Usage::
python -m click_and_drop_api.examples.view_order <order_id_or_reference>
"""
import os
import sys
from click_and_drop_api.simple import ClickAndDrop
API_KEY = os.environ["API_KEY"]
if len(sys.argv) != 2:
print(
"Usage: python -m click_and_drop_api.examples.view_order <order_id_or_reference>"
)
sys.exit(1)
identifier = sys.argv[1]
# treat numeric strings as integer order IDs
if identifier.isdigit():
identifier = int(identifier)
api = ClickAndDrop(API_KEY)
order = api.get_order(identifier)
if order is None:
print(f"Order not found: {identifier!r}")
sys.exit(1)
print(f"Order identifier : {order.order_identifier}")
print(f"Order reference : {order.order_reference}")
print(f"Created on : {order.created_on}")
print(f"Order date : {order.order_date}")
print(f"Printed on : {order.printed_on}")
print(f"Manifested on : {order.manifested_on}")
print(f"Shipped on : {order.shipped_on}")
print(f"Tracking number : {order.tracking_number}")
if order.packages:
print(f"Packages ({len(order.packages)}):")
for pkg in order.packages:
print(f" #{pkg.package_number} tracking: {pkg.tracking_number}")
Create and delete orders
The script below creates a new order and then deletes it.
#!/usr/bin/env python
"""Create an order and download it.
On CI, also delete the order again.
"""
from pprint import pprint
from click_and_drop_api.simple import (
ClickAndDrop,
CreateOrder,
RecipientDetails,
Address,
db,
LabelGeneration,
)
import os
from datetime import datetime, UTC
# navigate to https://business.parcel.royalmail.com/settings/channels/
# Configure API key authorization: Bearer
API_KEY = os.environ["API_KEY"]
api = ClickAndDrop(API_KEY)
# choose a new reference or else the API will reject the order
REFERENCE = "example-order-{now}".format(now=datetime.now(UTC).strftime("%Y%m%d%H%M%S"))
service = db.get(
"letter", "OLP2" if not api.is_oba() else "BPL2"
) # letter, 2nd class delivery
new_order = CreateOrder(
order_reference=REFERENCE,
is_recipient_a_business=False,
recipient=RecipientDetails(
address=Address(
full_name="Nicco Kunzmann",
company_name="",
address_line1="Wernlas",
address_line2="Talley",
address_line3="",
city="Llandeilo",
county="United Kingdom",
postcode="SA19 7EE",
country_code="GB",
),
phone_number="07726640000",
email_address="niccokunzmann" + "@" + "rambler.ru",
),
order_date=datetime.now(UTC),
subtotal=float(12), # 12 pounds
shipping_cost_charged=float(service.gross), # charge the same as Royal Mail
total=float(12 + service.gross),
currency_code="GBP",
postage_details=service.as_postage_details(),
packages=[service.as_package_request(weight_in_grams=80)],
## Label generation is only possible for OBA customers
label=LabelGeneration(
include_label_in_response=False,
include_cn=False,
include_returns_label=False,
)
if api.is_oba()
else None,
)
response = api.create_orders(new_order)
print(f"Orders created: {response.created_orders}")
print(f"Errors: {response.errors_count}")
for error in response.failed_orders:
print(f" {error.order.order_reference}: {error.errors}")
print("Getting the order from the API.")
order = api.get_order(REFERENCE)
print(f"Order Reference: {order.order_reference}")
print(f"Order Identifier: {order.order_identifier}")
pprint(order.to_dict())
# Delete the order when run in CI test
if "CI" in os.environ:
print("Deleting order.")
deleted_orders = api.delete_orders([REFERENCE])
print(f"Orders deleted: {deleted_orders.deleted_orders}")
print(f"Errors: {deleted_orders.errors}")
Output:
Orders created: [CreateOrderResponse(order_identifier=1012, order_reference='example-order-from-python-api', created_on=datetime.datetime(2026, 2, 11, 17, 32, 26, 763419, tzinfo=TzInfo(0)), order_date=datetime.datetime(2026, 2, 11, 17, 32, 26, 549854, tzinfo=TzInfo(0)), printed_on=None, manifested_on=None, shipped_on=None, tracking_number=None, packages=[CreatePackagesResponse(package_number=1, tracking_number=None)], label=None, label_errors=[], generated_documents=[])]
Errors: 0
Getting the order from the API.
Order Reference: example-order-from-python-api
Order Identifier: 1012
Deleting order.
Orders deleted: [DeletedOrderInfo(order_identifier=1012, order_reference='example-order-from-python-api', order_info=None)]
Errors: []
Package sizes and their shipping options
Several shipping options are available for each package size. There is no API for this, so the price and delivery speed is hard coded. You can view the table when you apply postage to an order. If the values are outdated, you are welcome to update them with a pull request and a screenshot of the table on the website.
This example prints all the available package sizes and their shipping options.
#!/usr/bin/env python
"""Print package size and shipping cost with options."""
from click_and_drop_api.simple import db
# Iterate package sizes in insertion order, then list each service under it
seen: dict = {}
for _o in db:
if _o.package_size_code not in seen:
seen[_o.package_size_code] = _o
for package_size_code, first in seen.items():
print("Package Code:", first.package_size_code)
print("Package Name:", first.package_name)
print("Package Max. Weight (grams):", first.package_max_weight_g)
print("Package Max. Height (mm):", first.height_mm)
print("Package Max. Width (mm):", first.width_mm)
print("Package Max. Length (mm):", first.depth_mm)
for option in db.for_package_size(package_size_code):
print(
f"\t{option.brand.ljust(14)} {option.service_code.ljust(10)} £{option.gross} \t{option.delivery_speed}"
)
Output:
Package Code: letter
Package Name: Letter
Package Max. Weight (grams): 100
Package Max. Height (mm): 5
Package Max. Width (mm): 165
Package Max. Length (mm): 240
Royal Mail OLP2 £0.87 48 hour (2 working days)
Royal Mail OLP1 £1.70 24 hour (next working day)
Royal Mail OLP1SF £3.60 24 hour (next working day)
Royal Mail OLP2SF £2.77 48 hour (2 working days)
Royal Mail SD1OLP £8.75 Guaranteed by 1pm next working day
Royal Mail SD2OLP £11.75 Guaranteed by 1pm next working day
Royal Mail SD3OLP £18.75 Guaranteed by 1pm next working day
Royal Mail ISOLP £3.40
Royal Mail ITSOLP £8.50
...
Print shipping countries for a service code
This example prints every country that accepts a given service code by testing each one via the API. Test orders are created and immediately deleted — no real shipments are made.
#!/usr/bin/env python
"""Print all countries that accept a given service code.
Uses test_shipping internally: a test order is created and immediately deleted
for each country, so no real shipments are made.
Arguments:
- service code (optional, defaults to OLP1)
"""
import os
import sys
from click_and_drop_api.simple import ClickAndDrop
API_KEY = os.environ["API_KEY"]
api = ClickAndDrop(API_KEY)
PACKAGE_SIZE = "letter"
SERVICE_CODE = "OLP1" if len(sys.argv) == 1 else sys.argv[1]
countries = api.get_countries_for_shipping(
package_size=PACKAGE_SIZE,
service_code=SERVICE_CODE,
weight_in_grams=50,
)
print(f"{PACKAGE_SIZE} with {SERVICE_CODE} ships to {len(countries)} countries:")
print(", ".join(countries))
Output:
letter with OLP1 ships to 228 countries:
AD, AE, AF, AG, AI, AL, AM, AO, AR, AT, AU, AW, AZ, BA, BB, BD, BE, BF, BG, BH, BI, BJ, BM, BN, BO, BQ, BR, BS, BT, BW, BY, BZ, CA, CC, CD, CF, CG, CH, CI, CL, CM, CN, CO, CR, CU, CV, CW, CX, CY, CZ, DE, DJ, DK, DM, DO, DZ, EC, EE, EG, ER, ES, ET, FI, FJ, FK, FM, FO, FR, GA, GB, GD, GE, GF, GG, GH, GI, GL, GM, GN, GP, GQ, GR, GS, GT, GU, GW, GY, HK, HN, HR, HT, HU, ID, IE, IL, IM, IN, IO, IQ, IR, IS, IT, JE, JM, JO, JP, KE, KG, KH, KI, KM, KN, KP, KR, KW, KY, KZ, LA, LB, LC, LI, LK, LR, LS, LT, LU, LV, MA, MC, MD, ME, MG, MK, ML, MM, MN, MO, MQ, MR, MS, MT, MU, MV, MW, MX, MY, MZ, NA, NC, NE, NF, NG, NI, NL, NO, NP, NR, NZ, OM, PA, PE, PF, PG, PH, PK, PL, PM, PN, PR, PT, PW, PY, QA, RE, RO, RS, RU, RW, SA, SB, SC, SD, SE, SG, SH, SI, SK, SL, SM, SN, SR, SS, ST, SV, SX, SZ, TC, TD, TF, TG, TH, TJ, TL, TM, TN, TO, TR, TT, TV, TW, TZ, UA, UG, US, UY, UZ, VA, VC, VE, VG, VI, VN, VU, WF, WS, ZA, ZM, ZW
Check if account is OBA
Some features — such as label generation and OBA-only service codes — require an OBA (Online Business Account). This example checks whether your account is an OBA by sending a test order with an OBA service code and immediately deleting it.
#!/usr/bin/env python
"""Check whether the account is an OBA (Online Business Account).
An OBA account has access to OBA-only service codes such as TPN24 (Tracked 24).
This is detected by attempting a test order with a service code — the order
is created and immediately deleted, so no shipment is made.
"""
from click_and_drop_api.simple import ClickAndDrop
import os
# navigate to https://business.parcel.royalmail.com/settings/channels/
# Configure API key authorization: Bearer
API_KEY = os.environ["API_KEY"]
api = ClickAndDrop(API_KEY)
if api.is_oba():
print("This is an OBA account — OBA service codes are available.")
else:
print("This is NOT an OBA account — only standard service codes are available.")
Output:
Create postage labels
! Labels are only available for OBA accounts !
The script below takes an order id or reference as an argument and generates a postage label for it.
#!/usr/bin/env python
"""Generate a label for an order number.
The order has to be created first.
You can use create_order.py to create an order.
View your orders: https://business.parcel.royalmail.com/orders/
Note: Label generation is only possible for OBA customers
"""
from click_and_drop_api.simple import ClickAndDrop
import os
import sys
from pathlib import Path
if len(sys.argv) < 2:
print(
f"""{Path(__file__).name} [ORDER_NUMBER_OR_REFERENCE]
ORDER_NUMBER_OR_REFERENCE: The order number or reference.
"""
)
print(__doc__)
exit(0)
# navigate to https://business.parcel.royalmail.com/settings/channels/
# Configure API key authorization: Bearer
API_KEY = os.environ["API_KEY"]
api = ClickAndDrop(API_KEY)
order_id = sys.argv[1]
if order_id.isdigit():
order_id = int(order_id)
label = api.get_label(order_id, "postageLabel", include_returns_label=False)
(Path(__file__).parent / "label.pdf").write_bytes(label)