
物流API对接实战:从入门到精通 - 快递鸟
行业情报站
来源:快递鸟 | 2026-05-19 18:46:50
作为电商平台、物流服务商或企业内部系统,物流 API 的对接是绕不开的必修课。本文将从实战角度出发,详细讲解物流 API 对接的完整流程,涵盖主流快递公司接口、物流跟踪、电子面单等核心功能,并提供 Python/JavaScript 示例代码,手把手教你完成对接。
物流API是提升电商、物流企业运营效率的关键服务。通过API接口, 可一次性批量查询多个运单轨迹,实时掌握物流动态。 快递鸟提供专业、稳定的物流查询API,支持国内外主流快递公司, 帮助企业降本增效。
点击展开查看答案
在正式开始之前,我们先理清一个核心问题:为什么物流 API 如此重要?
物流 API 的价值体现在三个层面:
无论你是电商系统开发者、物流 SaaS 产品经理,还是企业内部 IT,这个技能都能让你事半功倍。
目前国内市场主流的物流 API 服务商有以下几家:
| 服务商 | 优势 | 劣势 | 推荐场景 |
|---|---|---|---|
| 快递鸟 | 覆盖全面、支持 150+ 快递公司、支持电子面单 | 部分高阶功能收费 | 中小企业首选 |
| 菜鸟裹裹 | 阿里生态、数据量大 | 需淘宝/天猫店铺授权 | 电商平台 |
| 京东物流 | 速度快、数据准确 | 仅支持京东自营 | 京东商家 |
| 聚合数据 | 聚合多家、数据稳定 | 价格偏高 | 大型企业 |
本文以快递鸟(kdniao.com)为例进行讲解,原因是它覆盖最全面,接口文档清晰,适合作为入门学习的范本。
以快递鸟为例,步骤如下:
⚠️ 注意:免费套餐每日有调用次数限制(通常 1000 次/天),生产环境建议购买付费套餐。
快递鸟提供的核心接口包括:
📦 快递查询类
├── 实时快递查询(推荐)
├── 快递批量查询
└── 物流轨迹追踪
📄 电子面单类
├── 电子面单打印
├── 面单取消
└── 模板管理
🚚 增值服务类
├── 智能地址解析
├── 隐私快递
└── 签收回执
# Python 环境
pip install requests
# Node.js 环境
npm install axios
实时快递查询是最常用的接口,用于查询单个运单的物流状态。
请求地址: INLINECODE0
请求方式: POST
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| ShipperCode | String | ✅ | 快递公司编码,如 "SF"(顺丰)、"YTO"(圆通) |
| LogisticCode | String | ✅ | 运单号 |
| RequestType | String | ✅ | 固定值 "1002" |
| EBusinessID | String | ✅ | 商户 ID |
| DataSign | String | ✅ | 数据签名(详见下一节) |
| DataType | String | ❌ | 默认 "2"(JSON) |
为了保证数据安全,API 请求需要对参数进行签名验证:
import hashlib
import base64
import urllib.parse
import time
import requests
class KdNiaoClient:
"""快递鸟 API 客户端"""
def __init__(self, ebusiness_id: str, api_key: str):
self.ebusiness_id = ebusiness_id
self.api_key = api_key
self.api_url = "https://api.kdniao.com/Ebusiness/EbusinessOrderHandle.aspx"
def _generate_sign(self, request_data: str) -> str:
"""生成数据签名"""
# 1. 将请求参数与 API Key 拼接
sign_str = request_data + self.api_key
# 2. MD5 加密
m = hashlib.md5()
m.update(sign_str.encode('utf-8'))
sign = m.hexdigest()
# 3. Base64 编码
return base64.b64encode(sign.encode('utf-8')).decode('utf-8')
def _send_request(self, request_type: str, request_data: dict) -> dict:
"""发送 API 请求"""
# 序列化请求数据
data_str = json.dumps(request_data)
# 生成签名
data_sign = self._generate_sign(data_str)
# 构建请求参数
params = {
"RequestType": request_type,
"EBusinessID": self.ebusiness_id,
"DataSign": data_sign,
"RequestData": urllib.parse.quote(data_str),
"DataType": "2"
}
# 发送请求
response = requests.post(self.api_url, data=params, timeout=30)
return response.json()
import json
def query_express(client: KdNiaoClient, shipper_code: str, logistic_code: str):
"""查询快递物流"""
request_data = [
{
"ShipperCode": shipper_code, # 快递公司编码
"LogisticCode": logistic_code # 运单号
}
]
result = client._send_request("1002", request_data)
# 解析返回结果
if result.get("Success"):
shipper = result.get("ShipperName", "")
traces = result.get("Traces", [])
print(f"📦 快递公司: {shipper}")
print(f"📍 运单号: {logistic_code}")
print(f"📊 共 {len(traces)} 条物流记录")
print("-" * 50)
# 按时间倒序显示
for trace in reversed(traces):
accept_time = trace.get("AcceptTime", "")
accept_station = trace.get("AcceptStation", "")
print(f"🕐 {accept_time}")
print(f" {accept_station}")
print()
else:
print(f"❌ 查询失败: {result.get('Reason', '未知原因')}")
# 使用示例
if __name__ == "__main__":
client = KdNiaoClient(
ebusiness_id="your_ebusiness_id",
api_key="your_api_key"
)
# 查询顺丰快递
query_express(client, "SF", "SF1234567890")
{
"EBusinessID": "1234567",
"ShipperName": "顺丰速运",
"Success": true,
"LogisticCode": "SF1234567890",
"State": "2",
"StateName": "在途中",
"Traces": [
{
"AcceptTime": "2024-01-15 14:30:00",
"AcceptStation": "[深圳]快件已发往目的地[广州]",
"NextCity": "广州"
},
{
"AcceptTime": "2024-01-15 10:20:00",
"AcceptStation": "[深圳]已取件",
"NextCity": ""
}
]
}
物流状态码说明:
| 状态码 | 含义 |
|---|---|
| 0 | 暂无记录 |
| 1 | 揽收 |
| 2 | 在途中 |
| 3 | 签收 |
| 4 | 问题件 |
电子面单是对接中最复杂的部分,涉及面单模板、上传订单、获取面单图片等流程。
┌─────────────┐
│ 1. 充值 │ 预存面单数量
└──────┬──────┘
│
▼
┌─────────────┐
│ 2. 绑定 │ 绑定打印机和模板
│ 打印机模板 │
└──────┬──────┘
│
▼
┌─────────────┐
│ 3. 上传 │ 提交收发货人信息
│ 订单信息 │ 商家信息 + 消费者信息
└──────┬──────┘
│
▼
┌─────────────┐
│ 4. 获取 │ 获取面单图片/打印数据
│ 面单数据 │
└──────┬──────┘
│
▼
┌─────────────┐
│ 5. 打印 │ 本地打印或驱动打印
└─────────────┘
def upload_order(client: KdNiaoClient, order_data: dict):
"""
上传订单,获取电子面单
order_data 包含:
- ShipperCode: 快递公司编码
- OrderCode: 订单编号
- PayType: 支付方式 (1: 现付, 2: 到付)
- MonthCode: 月结账号(部分快递需要)
- Sender: 发件人信息
- Receiver: 收件人信息
- Commodity: 商品信息
"""
request_data = {
"OrderCode": order_data.get("OrderCode", ""),
"ShipperCode": order_data["ShipperCode"],
"PayType": order_data.get("PayType", 1),
"MonthCode": order_data.get("MonthCode", ""),
"IsNotice": 1, # 是否通知快递员上门取件
"Sender": {
"Name": order_data["Sender"]["Name"],
"Tel": order_data["Sender"]["Tel"],
"Mobile": order_data["Sender"].get("Mobile", ""),
"ProvinceName": order_data["Sender"]["ProvinceName"],
"CityName": order_data["Sender"]["CityName"],
"ExpAreaName": order_data["Sender"]["ExpAreaName"],
"Address": order_data["Sender"]["Address"]
},
"Receiver": {
"Name": order_data["Receiver"]["Name"],
"Tel": order_data["Receiver"]["Tel"],
"Mobile": order_data["Receiver"].get("Mobile", ""),
"ProvinceName": order_data["Receiver"]["ProvinceName"],
"CityName": order_data["Receiver"]["CityName"],
"ExpAreaName": order_data["Receiver"]["ExpAreaName"],
"Address": order_data["Receiver"]["Address"]
},
"Commodity": order_data.get("Commodity", [])
}
result = client._send_request("1007", [request_data])
if result.get("Success"):
order_result = result.get("Order", {})
print(f"✅ 订单提交成功!")
print(f"📋 订单号: {order_result.get('OrderCode')}")
print(f"🚚 快递公司: {order_result.get('ShipperName')}")
print(f"📄 运单号: {order_result.get('LogisticCode')}")
# 返回面单图片URL
return {
"order_code": order_result.get("OrderCode"),
"logistic_code": order_result.get("LogisticCode"),
"print_url": order_result.get("PrintTemplate"),
"origin_code": order_result.get("OriginCode"),
"dest_code": order_result.get("DestCode")
}
else:
print(f"❌ 订单提交失败: {result.get('Reason')}")
return None
# 使用示例
order = {
"OrderCode": "DD202401150001",
"ShipperCode": "YTO",
"PayType": 1,
"MonthCode": "1234567890",
"Sender": {
"Name": "张三",
"Tel": "020-12345678",
"Mobile": "13800138000",
"ProvinceName": "广东省",
"CityName": "广州市",
"ExpAreaName": "天河区",
"Address": "xxx路123号"
},
"Receiver": {
"Name": "李四",
"Tel": "021-87654321",
"Mobile": "13900139000",
"ProvinceName": "上海市",
"CityName": "上海市",
"ExpAreaName": "浦东新区",
"Address": "yyy路456号"
},
"Commodity": [
{
"GoodsName": "商品1",
"Quantity": 1
}
]
}
result = upload_order(client, order)
from PIL import Image
import requests
import io
def download_and_print(print_url: str, printer_name: str = "默认打印机"):
"""下载面单图片并打印"""
# 1. 下载面单图片
response = requests.get(print_url)
image = Image.open(io.BytesIO(response.content))
# 2. 显示预览
image.show()
# 3. 发送到打印机(Windows)
# image.save("temp_label.png")
# subprocess.run([
# "mspaint", "/pt", "temp_label.png", printer_name
# ])
print(f"✅ 面单已发送到打印机: {printer_name}")
def batch_print(client: KdNiaoClient, orders: list):
"""批量打印面单"""
results = []
for order in orders:
# 逐个上传并获取面单
result = upload_order(client, order)
if result and result.get("print_url"):
results.append(result)
# 下载并打印
download_and_print(result["print_url"])
# 避免请求过快
time.sleep(1)
print(f"\n📊 批量打印完成: {len(results)}/{len(orders)} 成功")
return results
地址解析是对接中的高阶功能,能将模糊地址(如"广东省深圳市南山区科技园")自动拆分为省市区结构化字段。
def parse_address(client: KdNiaoClient, address: str):
"""
智能地址解析
将文本地址解析为省、市、区、详细地址
"""
request_data = [{"Address": address}]
result = client._send_request("2002", request_data)
if result.get("Success"):
parsed = result.get("AddressList", [])[0]
return {
"province": parsed.get("ProvinceName", ""),
"city": parsed.get("CityName", ""),
"district": parsed.get("ExpAreaName", ""),
"detail": parsed.get("Address", ""),
"ad_code": parsed.get("CityCode", "") # 行政区划代码
}
else:
return None
# 使用示例
address = "广东省深圳市南山区科技园南区深投控大厦3楼"
parsed = parse_address(client, address)
print(f"📍 解析结果:")
print(f" 省份: {parsed['province']}")
print(f" 城市: {parsed['city']}")
print(f" 区县: {parsed['district']}")
print(f" 详情: {parsed['detail']}")
print(f" 代码: {parsed['ad_code']}")
原因: 该快递公司未开通或不支持当前功能
解决方案:
# 1. 检查快递公司编码是否正确
VALID_CODES = {
"SF": "顺丰速运",
"YTO": "圆通速递",
"ZTO": "中通快递",
"JD": "京东物流",
"EMS": "EMS",
"YT": "韵达快递"
}
# 2. 检查是否开通相应服务
# 登录快递鸟后台 → 产品服务 → 检查服务状态
原因: 未绑定打印机或模板
解决方案:
原因: 免费套餐调用量不足
解决方案:
# 1. 批量查询代替逐个查询
def batch_query(client: KdNiaoClient, codes: list):
"""批量查询,最多 100 条/次"""
for i in range(0, len(codes), 100):
batch = codes[i:i+100]
# 批量查询逻辑
time.sleep(1) # 控制频率
# 2. 升级付费套餐
# 3. 接入缓存机制,减少重复查询
排查步骤:
# 1. 确认 API Key 正确(注意大小写)
# 2. 确认签名算法顺序正确
# 3. 打印调试信息
def _generate_sign_debug(self, request_data: str) -> str:
sign_str = request_data + self.api_key
print(f"签名原串: {sign_str}") # 调试用,上线后删除
m = hashlib.md5()
m.update(sign_str.encode('utf-8'))
sign = m.hexdigest()
print(f"MD5结果: {sign}")
result = base64.b64encode(sign.encode('utf-8')).decode('utf-8')
print(f"Base64结果: {result}")
return result
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 业务系统 │────▶│ 物流中间件 │────▶│ 快递鸟 API │
│ (订单系统) │ │ (封装层) │ │ │
└─────────────┘ └──────┬──────┘ └─────────────┘
│
┌──────┴──────┐
│ 数据层 │
├─────────────┤
│ 本地缓存 │ 减少 API 调用
│ 数据库 │ 存储物流记录
│ 消息队列 │ 异步处理
└─────────────┘
from functools import lru_cache
import json
import redis
from typing import Optional
class LogisticsService:
"""物流服务封装"""
def __init__(self, client: KdNiaoClient, redis_client: redis.Redis):
self.client = client
self.redis = redis_client
self.cache_ttl = 3600 # 缓存 1 小时
@lru_cache(maxsize=1000)
def _get_shipper_name(self, shipper_code: str) -> str:
"""缓存快递公司名称"""
return self._fetch_shipper_name(shipper_code)
def query_with_cache(self, shipper_code: str, logistic_code: str) -> dict:
"""带缓存的物流查询"""
cache_key = f"logistics:{shipper_code}:{logistic_code}"
# 1. 先查缓存
cached = self.redis.get(cache_key)
if cached:
return json.loads(cached)
# 2. 缓存未命中,查 API
result = self._query_from_api(shipper_code, logistic_code)
# 3. 写入缓存
if result:
self.redis.setex(
cache_key,
self.cache_ttl,
json.dumps(result, ensure_ascii=False)
)
return result
def subscribe_callback(self, shipper_code: str, logistic_code: str, callback_url: str):
"""
订阅物流轨迹回调
当物流状态变化时,快递鸟会主动推送
"""
request_data = [{
"ShipperCode": shipper_code,
"LogisticCode": logistic_code,
"Callback": callback_url # 你的回调地址
}]
return self.client._send_request("1008", request_data)
from flask import Flask, request, jsonify
import hashlib
app = Flask(__name__)
WEB_API_KEY = "your_web_api_key" # 快递鸟后台配置的回调密钥
@app.route("/logistics/callback", methods=["POST"])
def logistics_callback():
"""接收快递鸟的物流状态推送"""
data = request.json
# 1. 验证签名
request_data = data.get("RequestData", "")
data_sign = data.get("DataSign", "")
# 重新计算签名验证
import base64
decoded = base64.b64decode(request_data).decode('utf-8')
expected_sign = base64.b64encode(
hashlib.md5((decoded + WEB_API_KEY).encode()).digest()
).decode()
if data_sign != expected_sign:
return jsonify({"Result": False, "ErrorCode": "签名验证失败"}), 403
# 2. 处理物流数据
traces = json.loads(decoded)
for trace in traces:
logistic_code = trace.get("LogisticCode")
state = trace.get("State")
state_name = trace.get("StateName")
traces_list = trace.get("Traces", [])
# 更新数据库
update_logistics_status(logistic_code, state, traces_list)
# 触发业务事件
if state == "3": # 签收
on_package_signed(logistic_code)
elif state == "4": # 问题件
on_exception(logistic_code, trace.get("UnNormalInfo"))
return jsonify({"Result": True})
def update_logistics_status(logistic_code, state, traces):
"""更新物流状态到数据库"""
# 实现你的数据库更新逻辑
pass
def on_package_signed(logistic_code):
"""签收事件处理"""
# 1. 更新订单状态
# 2. 发送通知(短信/微信)
# 3. 触发售后满意度调查
pass
def on_exception(logistic_code, unnormal_info):
"""异常件处理"""
# 1. 记录异常
# 2. 通知客服
# 3. 自动创建工单
pass
📌 入门三步走
├── 1️⃣ 申请 API 密钥
├── 2️⃣ 理解签名机制(MD5 + Base64)
└── 3️⃣ 调用快递查询接口(Hello World)
📌 进阶三板斧
├── 1️⃣ 电子面单对接(完整流程)
├── 2️⃣ 智能地址解析(提升用户体验)
└── 3️⃣ 轨迹订阅回调(实时推送)
📌 生产必备
├── 1️⃣ 本地缓存(减少 API 调用)
├── 2️⃣ 批量处理(提升效率)
├── 3️⃣ 异常处理(保障稳定性)
└── 4️⃣ 日志监控(快速定位问题)
如果你觉得这篇文章有帮助,欢迎点赞、收藏!有问题可以在评论区留言,我会尽量解答。
💡 提示:API 对接实战中,测试环境的充分验证是关键。建议先用少量订单测试完整流程,确认无误后再切换到生产环境。

相关产品推荐