SF Express (顺丰速运) for omnisense/omnibus: orders
with their waybills and cloud-printed labels, tracking and cancellation, through SF's open
platform. SF quotes by contract: prices come from the rates option. Every call is a form POST
to one endpoint, https://bspgw.sf-express.com/std/service (live) or
https://sfapi-sbox.sf-express.com/std/service (the sandbox, sandbox: true), naming the service:
| Operation | Service code |
|---|---|
| ship | EXP_RECE_CREATE_ORDER, then COM_RECE_CLOUD_PRINT_WAYBILLS |
| track | EXP_RECE_SEARCH_ROUTES |
| cancel | EXP_RECE_UPDATE_ORDER (dealType 2) |
Installation
composer require omnibus/sf-express
Requires PHP 8.2, omnisense/omnibus (2.x) and symfony/http-client (6.4, 7 or 8); given no
HTTP client, the factory makes one with HttpClient::create(). In a Symfony application with
Omnibus\Bridge\Symfony\OmnibusBundle, the factory is registered as soon as the package is
installed.
Credentials
- An account on SF's open platform: the customer code
(顾客编码,
partner_id) and its checkword (校验码), sandbox first. - The monthly account number (月结卡号) that pays for the shipments.
Each call sends partnerID, a random requestID, the serviceCode, a millisecond timestamp,
the JSON payload as msgData and its signature msgDigest =
base64(md5(urlencode(msgData + timestamp + checkword))) (Omnibus\SfExpress\Api::digest()),
with a 20-second timeout.
Options
| Option | Type | Default | Required | |
|---|---|---|---|---|
partner_id |
string | - | yes | the customer code |
checkword |
string | - | yes | the checkword that signs every call |
monthly_card |
?string | null |
no | the monthly account that pays; not sent when unset |
sandbox |
bool | false |
no | true: the sandbox endpoint |
rates |
list | - | no | the only way this gateway rates (the rates option) |
Shipment options read when booking (Shipment::$options):
| Option | Default | |
|---|---|---|
description |
商品 |
each cargo line's name |
instructions |
'' |
the order's remark, 100 characters |
template |
fm_76130_standard_<partner_id> |
the cloud print template code |
Capabilities
| Request | Supported |
|---|---|
Rating |
only with rates (configured prices) |
Shipping |
yes |
Tracking |
yes |
Pickup |
no |
GetSlip |
no |
Cancel |
yes |
Paperless |
no |
Behaviour
Answers. An envelope whose apiResultCode is not A1000 is an error; the apiResultData it
carries is JSON, and a result with success false is an error too; the result's msgData (else
the result) is the answer.
Ship. EXP_RECE_CREATE_ORDER, language zh-CN:
- the order id is the shipment's
reference, elseomnibus-and 12 random hexadecimal characters; - one cargo line per parcel: the
description, count 1, unit件, the weight in kilograms rounded to two decimals (at least 0.1), the parcel'svalueand currency when it has one; - the sender (contact type 1) and the recipient (contact type 2): company, name, the phone as telephone and mobile, country, the third street line as the province, city, the first two street lines joined as the address, postcode, email;
- the express type
Shipment::$serviceas an integer, else1; SF's types include 1 标快, 2 特快, 5 顺丰次晨, 6 顺丰即日; - payment method 1 on the
monthly_card; the number of parcels and the total weight; a return route label asked for (isReturnRoutelabel1); theinstructionsas remark.
The tracking number is the first waybill number. Then COM_RECE_CLOUD_PRINT_WAYBILLS, version
2.0, synchronous, PDF, with the template: the first file's base64 content is the label's
content, its url the label's url. When the print call fails, the booking still succeeds
without a document. The format is application/pdf; trackingUrl is
https://www.sf-express.com/chn/sc/waybill/waybill-detail/<number>. A response without a waybill
is a CarrierException (SF Express issued no waybill.). pickupPoint is not sent. The label's
carrierReference is the orderId the order was created under (the shipment's reference, else
the generated omnibus-<hex>): what cancel() takes.
Track. EXP_RECE_SEARCH_ROUTES by waybill number, in Chinese when the locale starts with
zh, else in English. The routes of the first answer: acceptTime read in Asia/Shanghai, the
remark as description, acceptAddress as location, opCode as code; sorted oldest first. The
status is the latest route's, read in this order:
opCode |
Or the remark contains | TrackingStatus |
|---|---|---|
80 |
已签收, signed, delivered |
DELIVERED |
44 |
派件, out for delivery |
OUT_FOR_DELIVERY |
70, 99 |
退回, return |
RETURNED |
33 |
异常, exception, unsuccessful |
EXCEPTION |
50, 54 |
已收取, picked up, collected |
IN_TRANSIT |
30, 31, 36, 604 |
已发出, 到达, arrived, departed |
IN_TRANSIT |
| no code and no remark | UNKNOWN |
|
| anything else | IN_TRANSIT |
No route is ever PENDING or AVAILABLE_FOR_PICKUP.
Cancel. EXP_RECE_UPDATE_ORDER with dealType 2 and the value given as orderId: pass the
label's carrierReference (or the shipment's reference, which it is when there was one), not
the waybill number, which SF does not know as an order. true when resStatus is 2 (or
absent).
Example
<?php
require __DIR__.'/vendor/autoload.php';
use Omnibus\Model\Address;
use Omnibus\Model\Parcel;
use Omnibus\Model\Shipment;
use Omnibus\SfExpress\SfExpressGatewayFactory;
use Symfony\Component\HttpClient\HttpClient;
$sf = (new SfExpressGatewayFactory(HttpClient::create()))->create([
'partner_id' => getenv('SF_PARTNER_ID'),
'checkword' => getenv('SF_CHECKWORD'),
'monthly_card' => getenv('SF_MONTHLY_CARD'),
'sandbox' => true,
]);
$shipment = new Shipment(
new Address('张三', ['科技园南区1号', '', '广东省'], '518057', '深圳市', 'CN', company: 'Glitch Art', phone: '13800138000'),
new Address('李四', ['朝阳区建国路1号', '', '北京市'], '100022', '北京市', 'CN', phone: '13900139000'),
[new Parcel(1200)],
service: '1',
reference: 'CMD-1042',
options: ['description' => '印刷品'],
);
$label = $sf->ship($shipment);
file_put_contents($label->trackingNumber.'.pdf', (string) $label->content);
echo $label->trackingNumber, ' ', $label->trackingUrl, "\n";
$tracking = $sf->track($label->trackingNumber, 'zh');
echo $tracking->status->value, ' ', $tracking->latest()?->location, "\n";
var_dump($sf->cancel($label->carrierReference)); // the order id, not the waybill
In Symfony:
omnibus:
gateways:
sf_express:
factory: sf_express
options:
partner_id: '%env(SF_PARTNER_ID)%'
checkword: '%env(SF_CHECKWORD)%'
monthly_card: '%env(SF_MONTHLY_CARD)%'
sandbox: '%env(bool:SF_SANDBOX)%'
rates:
- { service: '1', label: '顺丰标快', currency: CNY, countries: [CN], bands: { 1000: 1200, 5000: 2300 } }
Errors
Every failure is an Omnibus\Exception\CarrierException whose carrier is sf_express:
- an
apiResultCodeother thanA1000:apiErrorMsg(elseSF Express refused the call.), the code incarrierCode(A1002...); - a result with
successfalse: itserrorMsg(elseSF Express refused the request.), itserrorCodeincarrierCode; - an HTTP status of 400 or more, or an envelope that is not JSON:
SF Express answered HTTP <status>.; a result that is not JSON:SF Express answered with a result that is not JSON.; - a network error:
SF Express request failed: ...; SF Express issued no waybill.when no waybill number comes back.
A failed print call after the order is created is not raised (above). A missing partner_id or
checkword is an InvalidConfigException when the gateway is built; Pickup, GetSlip,
Paperless, and Rating without rates, raise RequestNotSupportedException before any call.
No failure carries the checkword: a network error's message has them masked as *** (the key
parameters of a URL too), its previous is a copy of the HTTP client's exception (the same class and
code), and no trace of the chain keeps its arguments, where the request's options stood (No secret
in a
failure).
Verified, and not
Tested offline: Tests/SfExpressGatewayTest.php (the digest; an order created with its waybill
and label, the partner id, the signature, the monthly card, the express type, the province from
the third street line, the contact types; tracking with its statuses and location; the
cancellation by the order id the order was created under, the label's carrierReference, with or
without a shipment reference; a refused call raised with its code) and Tests/SfExpressContractTest.php, the
family's conformance suite on recorded answers. The example above was run in the core's harness on
those recorded answers.
Not verified: the README and the factory mark the package unverified against the sandbox
until an account's keys are at hand; it is built from SF's published open platform documentation.
The cancellation by orderId alone follows SF's documentation of EXP_RECE_UPDATE_ORDER (the
customer order number is the order's key, the waybill list optional); not yet checked on the
sandbox.
Responsibility
An independent, unofficial client, not affiliated with S.F. Express (SF Holding Co., Ltd.). Whoever runs it complies with SF Express's terms (those of its open platform, accepted on registration: open.sf-express.com): see the README's Responsibility section.