mariopereira / fedex
A PHP interface to the Fedex's APIs
Requires
- php: >=7.4
- ext-curl: *
- ext-json: *
Requires (Dev)
- phpdocumentor/shim: ^3.10
- phpunit/phpunit: ^9.6
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
A PHP interface to the FedEx APIs.
Status: in development. The API of the package may still change.
| FedEx API | Class | State |
|---|---|---|
OAuth authorization (/oauth/token) |
FedexAuth |
Implemented |
Postal Code Validation (/country/v1/postal/validate) |
FedexPostalCodeValidation |
Implemented. Checked against the FedEx schema in schemas/, not yet against the live API |
Locations Search (/location/v1/locations) |
FedexLocationsSearch |
Implemented. Checked against the FedEx schema in schemas/, not yet against the live API |
Address Validation (/address/v1/addresses/resolve) |
FedexAddressValidation |
Implemented. Checked against the FedEx schema in schemas/, not yet against the live API |
Ship: Create Shipment (/ship/v1/shipments), Validate Shipment (/ship/v1/shipments/packages/validate), Cancel Shipment (/ship/v1/shipments/cancel), Create Tag (/ship/v1/shipments/tag), Cancel Tag (/ship/v1/shipments/tag/cancel/{shipmentid}) and Retrieve Async Ship (/ship/v1/shipments/results) |
FedexShip |
Implemented. Checked against the FedEx schema in schemas/, never called against the live API |
Basic Integrated Visibility (tracking): Track by Tracking Number (/track/v1/trackingnumbers), Track Multiple Piece Shipment (/track/v1/associatedshipments), Track by References (/track/v1/referencenumbers), Track by Transportation Control Number (/track/v1/tcn), Track Document (/track/v1/trackingdocuments) and Send Notification (/track/v1/notifications) |
FedexBasicTracking |
Implemented. Checked against the FedEx schema in schemas/, never called against the live API |
Consolidation: Create (/ship/v1/consolidations), Modify (PUT /ship/v1/consolidations), Create Consolidation Shipment (/ship/v1/consolidations/shipments), Shipment Results (/ship/v1/consolidations/results), Confirm (/ship/v1/consolidations/confirmations), Confirm Results (/ship/v1/consolidations/confirmationresults), Retrieve (/ship/v1/consolidations/retrieve), Delete Shipments (PUT /ship/v1/consolidations/shipments/delete) and Delete (PUT /ship/v1/consolidations/delete) |
FedexConsolidation |
Implemented. Checked against the FedEx schema in schemas/ (the top level of each request; the schema contradicts its own samples below that), never called against the live API |
Estimated Duty and Tax: Duties Taxes Estimates (/dutytax/v1/estimate) |
FedexDutyAndTax |
Implemented. Checked against the FedEx schema in schemas/, never called against the live API |
Service Availability: Retrieve Services and Transit Times (/availability/v1/transittimes), Retrieve Services and Packaging Options (/availability/v1/packageandserviceoptions) and Retrieve Special Service Options (/availability/v1/specialserviceoptions) |
FedexServiceAvailability |
Implemented. Checked against the FedEx schema in schemas/ (a number may also be given as a text that is a number, as the samples of the schema do), never called against the live API |
Rates and Transit Times: Retrieve Rates and Transit Times (/rate/v1/rates/quotes) |
FedexRateTransitTimes |
Implemented. Checked against the FedEx schema in schemas/ (a number may also be a text that is a number and a postal code an integer, and keys that the schema does not define are accepted, because the samples of the schema do all that), never called against the live API |
Pickup: Create Pickup (/pickup/v1/pickups), Check Pickup Availability (/pickup/v1/pickups/availabilities) and Cancel Pickup (PUT /pickup/v1/pickups/cancel) |
FedexPickup |
Implemented. Checked against the FedEx schema in schemas/, never called against the live API |
Global Trade: Retrieve Regulatory Details (/globaltrade/v1/shipments/regulatorydetails/retrieve) |
FedexGlobalTrade |
Implemented. Checked against the FedEx schema in schemas/, never called against the live API |
Ship End of Day Close: Ground End Of Day Close (PUT /ship/v1/endofday/) and Reprint End of the day (/ship/v1/endofday/) |
FedexEndOfDayClose |
Implemented. Checked against the FedEx schema in schemas/, never called against the live API |
Harmonized System (HS) Code Classification: Search Harmonized Code Data (/commodity/v2/hscodes/search) |
FedexHarmonizedCode |
Implemented. Checked against the FedEx schema in schemas/ (either the destination or the origin country of a commodity is enough, as the documentation says), never called against the live API |
Regulatory: Regulatory Profile (/regulatory/v1/profiles) |
FedexRegulatory |
Implemented. Checked against the FedEx schema in schemas/, never called against the live API |
Open Ship: Create Open Shipment (/ship/v1/openshipments/create), Modify Open Shipment (PUT /ship/v1/openshipments/), Confirm Open Shipment (/ship/v1/openshipments/), Modify Open Shipment Packages (PUT /ship/v1/openshipments/packages), Add Open Shipment Packages (/ship/v1/openshipments/packages), Delete Open Shipment Packages (PUT /ship/v1/openshipments/packages/delete), Retrieve Open Shipment Packages (/ship/v1/openshipments/packages/retrieve), Delete Open Shipment (PUT /ship/v1/openshipments/delete), Retrieve Open Shipment (/ship/v1/openshipments/retrieve) and Get Open Shipment Results (/ship/v1/openshipments/results) |
FedexOpenShip |
Implemented. Checked against the FedEx schema in schemas/ (the top level of each request: FedEx's collection contradicts the schema below that), never called against the live API |
Trade Documents Upload: Upload Document (/documents/v1/etds/upload), Upload Multiple Documents (/documents/v1/etds/multiupload), Upload Images (/documents/v1/lhsimages/upload), Upload Encoded Image (/documents/v1/lhsimages/encodedupload) and Upload Multiple Encoded Documents (/documents/v1/etds/encodedmultiupload), on the hosts of this API |
FedexTradeDocuments |
Implemented. Checked against the FedEx schema in schemas/ (the upload of an image sends the description and the rules as two parts, as the schema describes them), never called against the live API |
Requirements
- PHP 7.4 or later. The code uses PHP 7.4 syntax only and is also checked to run without deprecation notices on newer versions (up to PHP 8.5).
- The
curlandjsonextensions. cURL is used by the default HTTP transport (see HTTP transport). Thembstringextension is used when available.
Installation
composer require mariopereira/fedex
Usage
use Mariopereira\Fedex\Api\FedexPostalCodeValidation; use Mariopereira\Fedex\Auth\FedexAuth; use Mariopereira\Fedex\Exception\FedexException; use Mariopereira\Fedex\Misc\FedexUtils; // true = FedEx sandbox, false = production $auth = new FedexAuth(true, FedexUtils::GRANT_TYPE_CLIENT_CREDENTIALS, 'YOUR_CLIENT_ID', 'YOUR_CLIENT_SECRET'); $postal = new FedexPostalCodeValidation($auth); try { $shipDate = date('Y-m-d', strtotime('+1 day')); $result = $postal->validate('FDXG', 'US', 'TN', '38017', $shipDate); echo $result->output->cleanedPostalCode; } catch (FedexException $ex) { echo $ex->getMessage(), ' ', $ex->getErrorsAsString(); }
The access token is requested automatically, kept in the FedexAuth object until it expires, and renewed once if FedEx rejects it.
Postal code validation
The carrier code must be one of FedexPostalCodeValidation::CARRIER_CODES (FDXE, FDXG, FXSP, FDXC and FXCC). It is accepted in upper or lower case. Any other value is rejected before a request is sent.
The city is optional, except when the country is Australia (AU):
$result = $postal->validate('FDXE', 'AU', 'NSW', '2000', $shipDate, null, null, 'Sydney');
The optional arguments after the ship date are the routing code, the state/postal code mismatch check, and the city.
The request carries the API version, 1.1.1 unless you say otherwise: the schema marks version as optional and gives 1, 1 and 1 only as examples, so this is not a default that FedEx documents. Pass the numbers to the constructor or to setAPIversion() to send another version, or null to leave a number out. When all three are null the request has no version at all:
$postal->setAPIversion(null, null, null);
Locations search
Finds FedEx locations by address, by geographic coordinates or by phone number:
use Mariopereira\Fedex\Api\FedexLocationsSearch; $locations = new FedexLocationsSearch($auth); // by address: the address of the request, then any other field of the request $result = $locations->searchByAddress( ['postalCode' => '38017', 'countryCode' => 'US'], ['locationsSummaryRequestControlParameters' => ['maxResults' => 5], 'locationTypes' => ['FEDEX_OFFICE']] ); foreach ($result->output->locationDetailList ?? [] as $location) { echo $location->contactAndAddress->address->city ?? '', PHP_EOL; } // by geographic coordinates, or by (landline) phone number $result = $locations->searchByCoordinates(35.1495, -90.049, 'US'); $result = $locations->searchByPhoneNumber('9015551234', 'US');
For any other search, FedexLocationsSearch::newRequest() returns an empty request with every field of the API, named as in schemas/locations.json. Fill in what you need and pass it to search():
$request = FedexLocationsSearch::newRequest(); $request['location']['address']['postalCode'] = '38017'; $request['location']['address']['countryCode'] = 'US'; $request['sort']['criteria'] = 'DISTANCE'; $request['locationTypes'][] = 'FEDEX_OFFICE'; $result = $locations->search($request);
The request is checked before it is sent, and a problem is reported with a FedexLocationsSearchException:
- Fields set to
null, and empty lists, are left out. - A value of the wrong type and a value that is not in the lists of the schema are rejected, and a field that is not in the schema is sent (see Request validation). In the schema the latitude and the longitude are strings (
searchByCoordinates()converts numbers for you) andexcludeUnavailableLocationsis a string ('true'or'false'). - The country code is required. So is a city or a postal code when searching by address (the default), the latitude and the longitude when searching by geographic coordinates, and the phone number when searching by phone number.
- The
searchBy...()methods set the search criterion and the location themselves, so those two fields cannot be given as options.
Address validation
Validates and resolves addresses, up to 100 in a request:
use Mariopereira\Fedex\Api\FedexAddressValidation; $validation = new FedexAddressValidation($auth); // one address $result = $validation->validate([ 'streetLines' => ['7372 PARKRIDGE BLVD', 'APT 286'], 'city' => 'IRVING', 'stateOrProvinceCode' => 'TX', 'postalCode' => '75063-8659', 'countryCode' => 'US' ]); foreach ($result->output->resolvedAddresses ?? [] as $address) { echo $address->city ?? '', ' ', $address->classification ?? '', PHP_EOL; } // several addresses at once, and any other field of the request as an option $result = $validation->validateArray( [ ['streetLines' => ['24 Grosvenor Square'], 'city' => 'London', 'postalCode' => 'W1A 2LQ', 'countryCode' => 'GB'], ['streetLines' => ['Via Vittorio Veneto, 121'], 'city' => 'Roma', 'postalCode' => '00187', 'countryCode' => 'IT'] ], ['validateAddressControlParameters' => ['includeResolutionTokens' => true]] );
To send a clientReferenceId with an address, build the whole request. FedexAddressValidation::newRequest() and newAddress() return empty structures with every field of the API, named as in schemas/address-validation.json:
$request = FedexAddressValidation::newRequest(); $item = FedexAddressValidation::newAddress(); $item['address']['streetLines'][] = '7372 PARKRIDGE BLVD'; $item['address']['city'] = 'IRVING'; $item['address']['postalCode'] = '75063-8659'; $item['clientReferenceId'] = 'order-123'; $request['addressesToValidate'][] = $item; $result = $validation->resolve($request);
The request is checked before it is sent, and a problem is reported with a FedexAddressValidationException:
- Fields set to
null, and empty lists, are left out. - A value of the wrong type is rejected, and a field that is not in the schema is sent (see Request validation).
- Every address needs its street lines (a list). The city, the postal code (a text or an integer), the state and the country are optional, because the minimum fields vary among countries: FedEx checks them (and an empty street line, which FedEx decides about). The state, if given, has at most two characters and the country, if given, two letters.
inEffectAsOfTimestamp, if given, must be a real date written asYYYY-MM-DD.validate()andvalidateArray()setaddressesToValidatethemselves, so it cannot be given as an option.
Ship
Creates, validates and cancels shipments, creates and cancels tags, and retrieves the results of asynchronous shipments, with FedexShip.
create() buys a label, createTag() orders a pickup, and cancel() and cancelTag() void a shipment or a pickup: all four change real data in FedEx. The package never repeats them, except when FedEx answers that the access token is not valid (nothing was processed then). If an exception is thrown after the request was sent (a timeout, a network failure, or a response that is not valid) the operation may have been done: check it in FedEx before trying again. validate() and getAsyncResults() change nothing.
FedexShip::newCreateRequest(), newValidateRequest(), newCancelRequest(), newCreateTagRequest(), newCancelTagRequest() and newResultsRequest() return the structure of a request with the fields that the API requires, named as in schemas/ship.json. Any other field of the schema can be added:
use Mariopereira\Fedex\Api\FedexShip; $ship = new FedexShip($auth); $request = FedexShip::newCreateRequest(); $request['accountNumber']['value'] = '000000000'; $request['labelResponseOptions'] = 'URL_ONLY'; $request['requestedShipment']['shipper']['address'] = ['streetLines' => ['10 FedEx Parkway'], 'city' => 'Beverly Hills', 'countryCode' => 'US']; $request['requestedShipment']['shipper']['contact']['phoneNumber'] = '1234567890'; $request['requestedShipment']['recipients'][] = [ 'address' => ['streetLines' => ['1 Main St'], 'city' => 'Memphis', 'countryCode' => 'US'], 'contact' => ['phoneNumber' => '9012223333'] ]; $request['requestedShipment']['pickupType'] = 'USE_SCHEDULED_PICKUP'; $request['requestedShipment']['serviceType'] = 'FEDEX_GROUND'; $request['requestedShipment']['packagingType'] = 'YOUR_PACKAGING'; $request['requestedShipment']['totalWeight'] = 1.5; $request['requestedShipment']['shippingChargesPayment']['paymentType'] = 'SENDER'; $request['requestedShipment']['labelSpecification'] = ['labelStockType' => 'PAPER_4X6', 'imageType' => 'PDF']; $request['requestedShipment']['requestedPackageLineItems'][] = ['weight' => ['units' => 'LB', 'value' => 1.5]]; $result = $ship->create($request);
validate() takes the request of newValidateRequest(), which is the shipment without the account number and the label options. Note that FedEx's schema declares totalWeight of a validation as a whole number (an integer), and as a number with decimals when the shipment is created: a number with decimals is accepted in both (see Request validation), and totalWeight is optional, as FedEx's examples leave it out. cancelByTrackingNumber() cancels all the packages of a shipment:
$result = $ship->cancelByTrackingNumber('000000000', '794953555571');
cancel() takes the request of newCancelRequest() instead, for the other fields of the schema (emailShipment, senderCountryCode, deletionControl, version).
A tag is a return label that FedEx delivers to your customer, with a pickup (FedEx Express and FedEx Ground). createTag() takes the request of newCreateTagRequest(), which has the fields of a shipment plus the pickup times (pickupDetail). cancelTag() cancels a tag and its pickup, as long as the courier has not collected the package; the id of the shipment goes in the path of the call, and the schema does not say what it is, so it is sent as given:
use Mariopereira\Fedex\Api\FedexShip; $ship = new FedexShip($auth); $request = FedexShip::newCreateTagRequest(); $request['accountNumber']['value'] = '000000000'; $request['requestedShipment'] = [ 'shipper' => [ 'address' => ['streetLines' => ['1 Main St'], 'city' => 'Memphis', 'countryCode' => 'US'], 'contact' => ['phoneNumber' => '9012223333'] ], 'recipients' => [[ 'address' => ['streetLines' => ['10 FedEx Parkway'], 'city' => 'Beverly Hills', 'countryCode' => 'US'], 'contact' => ['phoneNumber' => '1234567890'] ]], 'pickupType' => 'USE_SCHEDULED_PICKUP', 'serviceType' => 'FEDEX_GROUND', 'packagingType' => 'YOUR_PACKAGING', 'totalWeight' => 1.5, 'shippingChargesPayment' => ['paymentType' => 'SENDER'], 'labelSpecification' => ['labelStockType' => 'PAPER_4X6', 'imageType' => 'PDF'], 'requestedPackageLineItems' => [['weight' => ['units' => 'LB', 'value' => 1.5]]], 'pickupDetail' => ['readyPickupDateTime' => '2020-07-03T09:00:00Z', 'latestPickupDateTime' => '2020-07-03T17:00:00Z'] ]; $result = $ship->createTag($request); $cancel = FedexShip::newCancelTagRequest(); $cancel['accountNumber']['value'] = '000000000'; $cancel['serviceType'] = 'FEDEX_GROUND'; $cancel['completedTagDetail'] = ['confirmationNumber' => '275', 'location' => 'NQAA', 'dispatchDate' => '2019-08-03']; $result = $ship->cancelTag('794953555571', $cancel);
When a shipment is created with asynchronous processing, FedEx answers with a job id, and the results are retrieved later with it. This does not create anything, so it can be repeated until the results are ready:
$result = $ship->getAsyncResultsByJobId('000000000', '89sxxxxx233ae24ff31xxxxx');
The request is checked before it is sent, and a problem is reported with a FedexShipException:
- Fields set to
null, and empty lists, are left out. - A value of the wrong type, a value that is not in the list of allowed values and a text longer than the schema allows are rejected, and a field that is not in the schema is sent (see Request validation).
totalWeightandcommercialInvoice, which the schema requires, are optional, because the examples of FedEx leave them out. - The required fields of the schema must be present. What the schema cannot express (for example the extra fields that some services or customs declarations need) is checked by FedEx, which answers with an error.
- The schema lists three fields of the shipment of a tag as required without ever defining them:
shipTimestamp,shippingLabelTypeandspecialServicesRequested. They are accepted with any value, and are not checked, so that they can be sent if FedEx asks for them. - The id given to
cancelTag()cannot be empty,.or... - The response is checked against the schema as well. A field that FedEx adds to the response is accepted, and an invalid response is reported with the response in the exception (
getData()).
Labels
FedEx returns each label already in the format asked for in labelSpecification.imageType (PDF, PNG, ZPLII or EPL2), as text in the field encodedLabel. Nothing is converted: FedexLabels finds the documents of a response and decodes them to the bytes of that format. Ask for 'labelResponseOptions' => 'LABEL' to get the label in the response (with URL_ONLY, FedEx returns a url instead).
use Mariopereira\Fedex\Misc\FedexLabels; $request['labelResponseOptions'] = 'LABEL'; $result = $ship->create($request); foreach (FedexLabels::documents($result) as $document) { if ($document['hasContent']) { FedexLabels::save($document, '/labels/' . $document['trackingNumber'] . '.pdf'); } }
documents()lists the documents of a create or async results response:output.transactionShipments[].shipmentDocuments[](scopeshipment, for example an invoice) andoutput.transactionShipments[].pieceResponses[].packageDocuments[](scopepackage, the labels). Each one hasscope,transactionIndex,pieceIndex,trackingNumber,contentType,docType,copiesToPrint,contentKey,url,encodedLabelandhasContent. What the response does not have is skipped. The response ofcreateTag()has no documents in this path.decode()returns the bytes of a document, from an element of the response or ofdocuments().save()writes them to a file and returns the number of bytes.save()refuses to replace a file unless its third argument istrue, and writes nothing when the document cannot be decoded.- A document without
encodedLabel, or with content that is not valid Base64, throws aFedexShipException. The schema does not say howencodedLabelis encoded: it is treated as standard Base64, and this has not been tried against FedEx. - The format check is optional.
decode()andsave()take$verifyFormat(and a$format, when it is not thedocTypeof the document): the bytes must then look like PDF (%PDF-in the first 1024 bytes), PNG (the PNG signature) or ZPL (^XAin the content), or aFedexShipExceptionis thrown.FedexLabels::isFormat()does the same check on any bytes. EPL2 cannot be checked and is refused when the check is asked for.
Basic tracking
FedexBasicTracking tracks shipments (Basic Integrated Visibility API, schemas/track.json). Tracking changes nothing in FedEx, so a request that failed with a timeout or a busy server can be retried (see Retries).
use Mariopereira\Fedex\Api\FedexBasicTracking; $tracking = new FedexBasicTracking($auth); $result = $tracking->trackByNumber('128667043726'); // several numbers, up to 30 in a request; false asks for the latest status only, without the scans $result = $tracking->trackByNumbers(['128667043726', '149331877648230'], false); foreach ($result->output->completeTrackResults as $complete) { foreach ($complete->trackResults as $track) { echo $track->trackingNumberInfo->trackingNumber, ': ', $track->latestStatusDetail->description ?? '', "\n"; } }
The other operations take the request of the schema. Each one has a method that makes the empty request (the required fields, with nested objects already in place), which is filled in and sent:
| Operation | Request | Method |
|---|---|---|
| Track by tracking number | newTrackingNumbersRequest() |
trackByTrackingNumbers() |
| Track the other packages of a multiple piece shipment | newAssociatedShipmentsRequest() |
trackAssociatedShipments() |
| Track by reference | newReferencesRequest() |
trackByReferences() |
| Track by Transportation Control Number | newTcnRequest() |
trackByTcn() |
| Track documents (signature proof of delivery, bill of lading, ...) | newDocumentsRequest() |
getTrackingDocuments() |
| Send tracking notifications | newNotificationRequest() |
sendNotification() |
$request = FedexBasicTracking::newReferencesRequest(); $request['referencesInformation']['value'] = '56754674567546754'; $request['referencesInformation']['accountNumber'] = '123456789'; $request['referencesInformation']['shipDateBegin'] = '2019-02-13'; $request['referencesInformation']['shipDateEnd'] = '2019-02-13'; $result = $tracking->trackByReferences($request);
- The request is checked before it is sent, as in the other APIs: the fields, their types and the values of the enums (the carrier code and the associated type) are the ones of the schema, the required fields must be present, and a request by tracking number has from 1 to 30 numbers (see Request validation for how loose the types are, and for the keys that the schema does not define, which are sent). Nothing is sent when the request is not valid. What the schema cannot express (for example that a reference needs the account number, or the destination country and postal code) is checked by FedEx.
- The values that the schema only describes (the type of a reference, the type and the format of a document, the type of a notification, the role, the event types) are not checked: they are sent as given.
- The response is checked against the schema too, and an invalid response throws a
FedexBasicTrackingExceptionwith the response ingetData(). A field that FedEx adds is accepted. sendNotification()sends emails. It is never retried, because repeating it would send them twice, and an invalid response says that the notification may have been sent.- The schema has a few defects, handled like this: the references request requires
shipDateEndDatebut its example, the other requests and the documentation writeshipDateEnd, soshipDateEndis the required one andshipDateEndDateis accepted as an optional field; the address requires a field,addressVerificationId, that it never defines, so it is optional and of any type; and two enums of the responses are defective (DelayDetail.subTypelists "EARTHQUAKE etc", andServiceCommitMessage.typehas a stray quote inUSPS_DELIVERING"), so those two fields are checked as text. - Never called against the live FedEx API: the schema is the only reference.
Consolidation
FedexConsolidation implements the Consolidation API (schemas/consolidation.json): creating, modifying, confirming, retrieving and deleting consolidations, and creating and deleting their shipments.
use Mariopereira\Fedex\Api\FedexConsolidation; $consolidation = new FedexConsolidation($auth); $key = ['consolidationType' => 'INTERNATIONAL_ECONOMY_DISTRIBUTION', 'index' => '12345', 'shipDatestamp' => '2022-03-21']; $result = $consolidation->retrieveByKey('123456789', $key); $request = FedexConsolidation::newCreateRequest(); $request['accountNumber']['value'] = '123456789'; $request['requestedConsolidation'] = ['shipper' => ['address' => ['countryCode' => 'US']]]; // see the schema and its samples $problems = FedexConsolidation::checkRequest('create', $request); // what does not match the schema, nothing is sent $result = $consolidation->createConsolidation($request);
| Operation | Request | Method |
|---|---|---|
| Create Consolidation | newCreateRequest() |
createConsolidation() |
| Modify Consolidation | newModifyRequest() |
modifyConsolidation() |
| Create Consolidation Shipment | newCreateShipmentRequest() |
createConsolidationShipment() |
| Consolidation Shipment Results | newShipmentResultsRequest() |
getShipmentResults() and getShipmentResultsByJobId() |
| Confirm Consolidations | newConfirmRequest() |
confirmConsolidation() |
| Confirm Consolidation Results | newConfirmResultsRequest() |
getConfirmationResults() and getConfirmationResultsByJobId() |
| Retrieve Consolidation | newRetrieveRequest() |
retrieveConsolidation() and retrieveByKey() |
| Delete Consolidation Shipments | newDeleteShipmentsRequest() |
deleteConsolidationShipments() |
| Delete Consolidation | newDeleteRequest() |
deleteConsolidation() and deleteByKey() |
- Creating, modifying, confirming and deleting change real data in FedEx, and creating a shipment of a consolidation buys its labels. The package never repeats those requests, except when FedEx answers that the access token is not valid (nothing was processed then). If an exception is thrown after the request was sent (a timeout, a network failure, or a response that is not valid), the operation may have been done: check it in FedEx before trying again. Modify is treated like the others, although the same modification repeated would probably do no harm. Only the results of a shipment, the results of a confirmation and the retrieval change nothing, so only those can be retried (see Retries).
- A request is checked at its top level only. The schema contradicts its own sample payloads in the nested objects: only 3 of its 19 samples pass the whole of it (for example it requires
consolidationKey.dateandtype, and the samples sendconsolidationTypeandshipDatestamp). So the required fields and the types of the fields are checked at the top level (a key that the schema does not define is sent, see Request validation), and each nested object or list is sent as it comes: FedEx checks the rest and answers with its errors. Nothing is sent when the top level is not valid. The 19 samples of the schema all pass this check. FedexConsolidation::checkRequest()andcheckResponse()check the whole of the schema and return the problems, each with the path of the field, without stopping anything (nothing is sent). The operation is one ofcreate,modify,createShipment,shipmentResults,confirm,confirmResults,retrieve,deleteShipmentsanddelete. A problem that they report is not necessarily one that FedEx refuses, given what the samples of the schema are.- The response is checked at its top level, and an invalid response throws a
FedexConsolidationExceptionwith the response ingetData(). A field that FedEx adds is accepted. - The schema has a few defects, handled like this in the whole check:
RequestedConsolidationShipmentrequires a fieldrecipientthat is never defined (the field isrecipients, and it is not required), sorecipientis accepted with any value; andLabel.typeandShippingDocument.typehave an enum of one value, the whole list joined in a text, so they are checked as text. - Never called against the live FedEx API: the schema is the only reference.
Estimated duty and tax
FedexDutyAndTax estimates the duties, the taxes and the fees of a shipment (Estimated Duty and Tax API, schemas/dutiestaxes.json). An estimate changes nothing in FedEx, so a request that failed with a timeout or a busy server can be retried (see Retries).
use Mariopereira\Fedex\Api\FedexDutyAndTax; $duties = new FedexDutyAndTax($auth); $shoes = FedexDutyAndTax::newCommodity(); $shoes['description'] = 'Athletic Shoes'; $shoes['quantity'] = 1; $shoes['unitPrice'] = ['amount' => 10.39, 'currencyCode' => 'USD']; // from the countries and the commodities (the shipper and the recipient have a country code of two letters) $result = $duties->estimateForCommodities('IN', 'US', [$shoes], ['shipmentPurpose' => 'GIFT']); // or with the whole request, for example to give a postal code or a state $request = FedexDutyAndTax::newRequest(); $request['shipper']['countryCode'] = 'IN'; $request['recipient'] = ['countryCode' => 'CA', 'stateOrProvinceCode' => 'AB']; $request['commodities'][] = $shoes; $result = $duties->estimate($request); foreach ($result->output->edtDetails as $details) { echo $details->summary->totalLandedCost, ' ', $details->summary->currency, "\n"; }
- The request is checked before it is sent, as in the other APIs: the fields, their types, the lengths (the name of a commodity up to 256 characters, its description up to 512, the country of manufacture 2) and the values of
shipmentPurpose(DOCUMENTS,FOR_RESALE,NOT_FOR_RESALE,GIFTorRETURN) are the ones of the schema, and the required fields must be present: the country of the shipper and of the recipient, and for each commodity its description, its quantity and its unit price with the amount and the currency code. Nothing is sent when the request is not valid. What the schema cannot express (the state or province code, which FedEx requires for Canada and Brazil) is checked by FedEx. - The country codes of the shipper and the recipient must be two letters: the schema has no length, but its description says "the two-letter code used to identify a country".
- The schema asks for the header
x-content-type: application/json, which is sent besidesContent-Type. - The response is checked against the schema too, and an invalid response throws a
FedexDutyAndTaxExceptionwith the response ingetData(). A field that FedEx adds is accepted. - The examples of the schema (three requests and a response) are all accepted by these checks.
- Never called against the live FedEx API: the schema is the only reference.
Service availability
FedexServiceAvailability asks FedEx what is available for a shipment: the services and their transit times, the services and the packaging options, and the special service options (Service Availability API, schemas/service-availability.json). The three operations change nothing in FedEx, so a request that failed with a timeout or a busy server can be retried (see Retries).
use Mariopereira\Fedex\Api\FedexServiceAvailability; $availability = new FedexServiceAvailability($auth); // between two addresses, with one package (a weight of 10.5 pounds here) $from = ['postalCode' => '38117', 'countryCode' => 'US']; $to = ['postalCode' => '75201', 'countryCode' => 'US']; $result = $availability->getTransitTimesBetween($from, $to, 10.5); foreach ($result->output->transitTimes[0]->transitTimeDetails as $detail) { echo $detail->serviceType, ' ', $detail->serviceName, "\n"; } // the same for the packaging options and for the special service options $options = $availability->getPackageAndServiceOptionsBetween(['countryCode' => 'US'], ['countryCode' => 'CA'], ['FDXE']); $special = $availability->getSpecialServiceOptionsBetween(['countryCode' => 'US'], ['countryCode' => 'CA'], 5, 'KG'); // or with the whole request, to use any field of the schema $request = FedexServiceAvailability::newTransitTimesRequest(); $request['requestedShipment']['shipper']['address'] = $from; $request['requestedShipment']['recipients'][] = ['address' => $to]; $request['requestedShipment']['packagingType'] = 'YOUR_PACKAGING'; $request['requestedShipment']['requestedPackageLineItems'][] = ['weight' => ['units' => 'LB', 'value' => 10.5]]; $result = $availability->getTransitTimes($request);
- The request is checked before it is sent, as in the other APIs: the fields, their types and their allowed values are the ones of the schema, and the required fields must be present. Nothing is sent when the request is not valid.
newTransitTimesRequest(),newPackageAndServiceOptionsRequest()andnewSpecialServiceOptionsRequest()give the required fields. Note that the recipient is a list (recipients) in the transit times and in the packaging options, and one object (recipient) in the special service options. The schema requires nothing at the top of the transit times request, but every sample hasrequestedShipment, so the template has it.- A number can be given as a number or as a text that is a number (
'10.5'), because the samples of the schema send the weight and the customs value as text although the schema says number. This also applies to the weight and the money of a response. - The methods
...Between()take the addresses as arrays, with the fields of the address in the schema.requestedShipmentandcarrierCodescannot be given in their$options: they are set by the method. - The response is checked against the schema too, and an invalid response throws a
FedexServiceAvailabilityExceptionwith the response ingetData(). A field that FedEx adds is accepted. - The samples of the schema (eight requests) are all accepted by these checks. The schema has no sample of a response.
- Never called against the live FedEx API: the schema is the only reference.
Rates and transit times
FedexRateTransitTimes asks FedEx for the rates, and the transit times, of the services that are available for a shipment (Rates and Transit Times API, schemas/rate.json). A quote changes nothing in FedEx, so a request that failed with a timeout or a busy server can be retried (see Retries).
use Mariopereira\Fedex\Api\FedexRateTransitTimes; $rates = new FedexRateTransitTimes($auth); // between two addresses, with one package (a weight of 10.5 pounds here) $from = ['postalCode' => '38117', 'countryCode' => 'US']; $to = ['postalCode' => '75201', 'countryCode' => 'US']; $result = $rates->getRatesBetween('123456789', $from, $to, 10.5); foreach ($result->output->rateReplyDetails as $reply) { echo $reply->serviceType, ' ', $reply->serviceName, "\n"; } // or with the whole request, to use any field of the schema $request = FedexRateTransitTimes::newRatesRequest(); $request['accountNumber']['value'] = '123456789'; $request['requestedShipment']['shipper']['address'] = $from; $request['requestedShipment']['recipient']['address'] = $to; $request['requestedShipment']['pickupType'] = 'DROPOFF_AT_FEDEX_LOCATION'; $request['requestedShipment']['requestedPackageLineItems'][] = ['weight' => ['units' => 'LB', 'value' => 10.5]]; $result = $rates->getRates($request);
- The request is checked before it is sent, as in the other APIs: the types and the allowed values of the fields are the ones of the schema, and the required fields must be present: in the shipment the shipper and the recipient (each with the postal code and the country code of its address), the pickup type and the packages (each with the weight and its unit). The account number and the customs commodities, which the schema requires, are optional, because the samples of FedEx leave them out. Nothing is sent when the request is not valid.
- The samples of the schema contradict the schema, and the checks follow the samples: a number can be given as a number or as a text that is a number (
'10.5'), as the samples do for the weight; a postal code can be a text or an integer (75063); and a field that the schema does not define is accepted and sent, so FedEx decides about it. This also applies to the weight and the money of a response. - All twenty two samples of the schema are accepted (a
nullis left out of the request, as for the other APIs): the one for Freight sendsgroupPackageCountas a text where the schema says integer, and the one for Intra Europe has no account number. getRatesBetween()takes the addresses as arrays, with the fields of the address in the schema.accountNumberandrequestedShipmentcannot be given in its$options: they are set by the method.- The response is checked against the schema too, and an invalid response throws a
FedexRateTransitTimesExceptionwith the response ingetData(). A field that FedEx adds is accepted. The schema has no sample of a response. - Never called against the live FedEx API: the schema is the only reference.
Pickup
FedexPickup checks when FedEx can pick up a package, schedules a FedEx Express or Ground pickup, and cancels it (Pickup Request API, schemas/pickup.json).
use Mariopereira\Fedex\Api\FedexPickup; $pickup = new FedexPickup($auth); // when can FedEx come? (this only asks, and can be repeated) $result = $pickup->checkAvailabilityAt(['postalCode' => '38117', 'countryCode' => 'US'], ['FDXE', 'FDXG']); // schedule a pickup $request = FedexPickup::newCreateRequest(); $request['associatedAccountNumber']['value'] = '123456789'; $request['originDetail']['pickupLocation']['contact'] = ['personName' => 'John Taylor', 'phoneNumber' => '9013577890']; $request['originDetail']['pickupLocation']['address'] = [ 'streetLines' => ['10 FedEx Parkway'], 'city' => 'Beverly Hills', 'stateOrProvinceCode' => 'CA', 'postalCode' => '90210', 'countryCode' => 'US', ]; $request['originDetail']['readyDateTimestamp'] = '2026-11-03T09:00:00Z'; $request['originDetail']['customerCloseTime'] = '17:00:00'; $request['carrierCode'] = 'FDXE'; $result = $pickup->createPickup($request); $code = $result->output->pickupConfirmationCode; // cancel it with the confirmation code $pickup->cancelByConfirmation('123456789', $code, '2026-11-03');
createPickup()asks FedEx to send a driver, andcancelPickup()cancels one. They are never retried (see Retries), except when FedEx answers 401 and the request is sent again with a fresh token, because repeating them could schedule or cancel twice. If one of them fails after it was sent (a timeout, a network failure, or a response that is not valid), the exception says so: the pickup may or may not have been scheduled, so check it before trying again. Checking the availability changes nothing, so it is retried like the other read-only operations.- The request is checked before it is sent, as in the other APIs (see Request validation): the types and the allowed values of the fields are the ones of the schema, and the required fields must be present. Nothing is sent when the request is not valid. The required fields are the account number, the carrier (
FDXEorFDXG) and the origin detail for a pickup; the pickup address, the carriers, the country relationship and the request type for the availability; and the confirmation code, the date and the account number to cancel. shipmentAttributesof the availability is an object in the schema, but two of its samples send a list with one object: both are accepted (the content of the list is not checked).- The minimum length of the street lines is not checked, and the state of the pickup location and the postal code of the address of the availability are optional, although the schema requires them, because the minimum fields of an address vary among countries (the examples of FedEx leave them out for some countries): FedEx decides.
- The response is checked against the schema too, and an invalid response throws a
FedexPickupExceptionwith the response ingetData(). A field that FedEx adds is accepted. The schema has no sample of a response. - The eight samples of the schema are all accepted by these checks.
- Never called against the live FedEx API: the schema is the only reference.
Global trade
FedexGlobalTrade retrieves the regulatory documents that a shipment needs, and the advisories that apply to it, before it is shipped (Global Trade API, schemas/global-trade.json). It only asks FedEx and changes nothing, so a request that failed with a timeout or a busy server can be retried (see Retries).
use Mariopereira\Fedex\Api\FedexGlobalTrade; $globalTrade = new FedexGlobalTrade($auth); // between two countries, with a carrier and any other field of the request $result = $globalTrade->retrieveRegulatoryDetailsFor(['countryCode' => 'IN'], ['countryCode' => 'CA'], 'FDXE', [ 'customsClearanceDetail' => ['commodities' => [['harmonizedCode' => '580122']]], ]); foreach ($result->output->userMessages as $message) { echo $message->text, "\n"; } // or with the whole request $request = FedexGlobalTrade::newRequest(); $request['originAddress']['countryCode'] = 'IN'; $request['destinationAddress']['countryCode'] = 'CA'; $request['carrierCode'] = 'FDXE'; $result = $globalTrade->retrieveRegulatoryDetails($request);
- The request is checked before it is sent, as in the other APIs (see Request validation): the types and the allowed values of the fields are the ones of the schema, and the required fields must be present: the country code of the origin and of the destination, and the carrier. Nothing is sent when the request is not valid.
- The value of a weight (of a commodity, and the total weight) is optional, although the schema requires it: most requests of FedEx's collection leave it out (they send
values, a typo, which is sent as it is). The units of a weight are still required. - The sample of the schema leaves the amount of the customs value blank (
''), which the loose checks accept. retrieveRegulatoryDetailsFor()refusesoriginAddress,destinationAddressandcarrierCodein its$options: they are set by the method.- The response is checked against the schema too, and an invalid response throws a
FedexGlobalTradeExceptionwith the response ingetData(). A field that FedEx adds is accepted. The schema has no sample of a response. - Never called against the live FedEx API: the schema is the only reference.
End of day close
FedexEndOfDayClose closes the FedEx Ground shipments of the day, which gives the Ground Manifest to print and hand to the FedEx driver before the shipments are tendered, and reprints the reports of a close (Ship End of Day Close API, schemas/close.json).
use Mariopereira\Fedex\Api\FedexEndOfDayClose; $endOfDay = new FedexEndOfDayClose($auth); // close the ground shipments of the day (this closes them in FedEx) $result = $endOfDay->closeGroundFor('123456789', '2026-11-03', [ 'closeDocumentSpecification' => ['closeDocumentTypes' => ['MANIFEST']], ]); foreach ($result->output->closeDocuments as $document) { echo $document->type, "\n"; } // reprint the reports of the closes of a day (up to three days back) $result = $endOfDay->reprintReportsFor('123456789', '2026-11-03'); // or with the whole request, for example to reprint by tracking number $request = FedexEndOfDayClose::newReprintRequest(); $request['accountNumber']['value'] = '123456789'; $request['reprintOption'] = 'BY_TRACKING_NUMBER'; $request['trackingNumber'] = '794953555571'; $result = $endOfDay->reprintReports($request);
closeGround()closes the shipments in FedEx. It is never retried (see Retries), except when FedEx answers 401 and the request is sent again with a fresh token. If it fails after it was sent (a timeout, a network failure, or a response that is not valid), the exception says so: the close may have been done, so check it before trying again. The reprint changes nothing, so it is retried like the other read-only operations.- The close is a
PUTand the reprint aPOST, both to/ship/v1/endofday/(with the trailing slash of the schema). - The request is checked before it is sent, as in the other APIs (see Request validation). The schema requires no field in either request, so only the types and the allowed values are checked (
closeReqType,groundServiceCategory,reprintOption, the document types and formats); FedEx checks the rest. The templates and the...For()methods fill in the key inputs that the documentation lists: the request type (GCDRto close,RGCDRto reprint), the ground service category (GROUND), the account number and the close date. closeGroundFor()andreprintReportsFor()refuse, in their$options, the fields that they set themselves.- The samples of the schema send the account number as an integer, which the loose checks accept. 14 of the 15 requests of FedEx's collection are accepted; the other one is a negative test (an empty
reprintOption). - The response is checked against the schema too, and an invalid response throws a
FedexEndOfDayCloseExceptionwith the response ingetData(). A field that FedEx adds is accepted. The schema has no sample of a response. - Never called against the live FedEx API: the schema is the only reference.
Harmonized code
FedexHarmonizedCode suggests the Harmonized System (HS) codes of commodities, from their description and their destination or origin country (HS Code Classification API, schemas/harmonized-code.json). The search is powered by artificial intelligence, and FedEx says that its results are only suggestions: validate them before using them in a shipment. It changes nothing, so a request that failed with a timeout or a busy server can be retried (see Retries).
use Mariopereira\Fedex\Api\FedexHarmonizedCode; $hsCodes = new FedexHarmonizedCode($auth); // one commodity, with its destination countries and its origin country (either one is enough) $result = $hsCodes->searchFor('Footwear with outer soles of rubber or plastics. Sports footwear', ['US', 'CA'], 'IN', [ 'material' => 'leather', ]); foreach ($result->output->hsClassifications as $classification) { foreach ($classification->subheadingOptions as $option) { echo $classification->countryCode, ' ', $option->code, ' ', $option->confidenceScore, "\n"; } } // or several commodities in one request $request = FedexHarmonizedCode::newRequest(); $shoes = FedexHarmonizedCode::newClassification(); $shoes['commodityDescription'] = 'custom shoes'; $shoes['destinationCountryCodes'][] = 'GB'; $request['hsClassify'][] = $shoes; $result = $hsCodes->searchHarmonizedCodes($request);
- The request is checked before it is sent, as in the other APIs (see Request validation): the types, the lengths (the name 256, the description 512, the country codes 2, the product id 100) and the required fields of the schema. Nothing is sent when the request is not valid.
- The schema requires both the destination countries and the origin country of a commodity, but the documentation says that either one is required, and the minimum examples of the schema send only one: so either one is enough, and a commodity without either is refused. At least one commodity is required (the schema does not say so, but the documentation requires the description and a country of a commodity).
- The schema asks for the header
x-content-type: application/json, which is sent besidesContent-Type. - The response is checked against the schema too, and an invalid response throws a
FedexHarmonizedCodeExceptionwith the response ingetData(). A field that FedEx adds is accepted. The four request examples and the three response examples of the schema are all accepted by these checks. - Never called against the live FedEx API: the schema is the only reference.
Regulatory
FedexRegulatory stores the regulatory compliance data of products, so that shipments can reference it in commodities[].regulatoryDetails[]: CPSC certificates of compliance for imports into the United States, and the product identifiers of EU de minimis consignments (Regulatory API, schemas/regulatory.json).
use Mariopereira\Fedex\Api\FedexRegulatory; $regulatory = new FedexRegulatory($auth); $profile = FedexRegulatory::newProfile(); $profile['regulationCode'] = 'EU_DE_MINIMIS'; $profile['productId'] = 'TSHIRT-L-001'; $profile['productIdType'] = 'SKU'; $profile['countryOfImport'] = 'FR'; $profile['details'][] = ['standardManufacturerProductId' => 'GTIN 00000006', 'merchantProductId' => '12345']; // one profile (this stores it in FedEx) $result = $regulatory->createProfile($profile); echo $result->output->message, "\n"; // or up to 25 profiles in one request $tShirtM = array_replace($profile, ['productId' => 'TSHIRT-M-001']); $result = $regulatory->createProfiles([$profile, $tShirtM]);
- This stores data in FedEx. It is never retried (see Retries), except when FedEx answers 401 and the request is sent again with a fresh token. If it fails after it was sent (a timeout, a network failure, or a response that is not valid), the exception says so: the profiles may have been stored, so check it before trying again.
- The request is a JSON list of profiles. It is checked before it is sent, as in the other APIs (see Request validation): from 1 to 25 profiles, each one with its regulation (
CPSCorEU_DE_MINIMIS), its product id and its type, its country of import and at least one detail, and the types, the lengths and the allowed values of the schema. What each regulation needs in its details is checked by FedEx. Nothing is sent when the request is not valid. - Three integer fields have a length in the schema (
gtinUpcCode14,phoneExtension5,lotNumberQualifier1): it is not checked, FedEx checks it. A GTIN with leading zeros can be given as text ('00012345678905'). The contact'sphoneNumberhas no length limit, because an example of the schema sends an international number longer than the 15 characters of the schema, andlotNumberQualifieralso takes the texts'1'and'2'. - The response is checked against the schema too, and an invalid response throws a
FedexRegulatoryExceptionwith the response ingetData(). A field that FedEx adds is accepted. The six request examples of the schema are all accepted. - Never called against the live FedEx API: the schema is the only reference.
Trade documents upload
FedexTradeDocuments uploads the customs documents of an electronic trade documents (ETD) shipment, before or after it is shipped, and the letterhead and signature images used in the documents that FedEx generates (Trade Documents Upload API, schemas/upload-documents.json).
use Mariopereira\Fedex\Api\FedexTradeDocuments; $documents = new FedexTradeDocuments($auth); // one document (this stores it in FedEx) $document = FedexTradeDocuments::newDocument(); $document['workflowName'] = 'ETDPreshipment'; $document['name'] = 'invoice.pdf'; $document['contentType'] = 'application/pdf'; $document['meta']['shipDocumentType'] = 'COMMERCIAL_INVOICE'; $document['meta']['originCountryCode'] = 'US'; $document['meta']['destinationCountryCode'] = 'IN'; $result = $documents->uploadDocument($document, 'invoice.pdf', file_get_contents('invoice.pdf'), 'application/pdf'); echo $result->output->meta->docId, "\n"; // up to five documents of a shipment at once: the information, then each file as [file name, content, media type] $result = $documents->uploadDocuments([ 'workflowName' => 'ETDPreshipment', 'carrierCode' => 'FDXE', 'originCountryCode' => 'US', 'destinationCountryCode' => 'CA', 'metaData' => [ ['fileName' => 'invoice.pdf', 'contentType' => 'application/pdf', 'shipDocumentType' => 'COMMERCIAL_INVOICE'], ], ], [ ['invoice.pdf', file_get_contents('invoice.pdf'), 'application/pdf'], ]); // a letterhead or a signature $image = FedexTradeDocuments::newImageDocument(); $image['referenceId'] = '1234'; $image['name'] = 'signature.png'; $image['contentType'] = 'image/png'; $image['meta']['imageType'] = 'SIGNATURE'; $image['meta']['imageIndex'] = 'IMAGE_1'; $result = $documents->uploadImage($image, ['workflowName' => 'LetterheadSignature'], 'signature.png', file_get_contents('signature.png'), 'image/png');
- Every upload stores data in FedEx. None is retried (see Retries), except when FedEx answers 401 and the request is sent again with a fresh token. If one fails after it was sent (a timeout, a network failure, or a response that is not valid), the exception says so: the files may have been stored, so check it before trying again.
- This API has its own hosts,
https://documentapitest.prod.fedex.com/sandbox/in the sandbox andhttps://documentapi.prod.fedex.com/in production (from the documentation of the API); the token is the same as for the other APIs. uploadDocument(),uploadDocuments()anduploadImage()send amultipart/form-datarequest: the description as JSON parts, then the files as they are.uploadEncodedImage()anduploadEncodedDocuments()send a JSON request with the files encoded in Base64, named as in the schema.- The upload of an image sends three parts,
document,rulesandattachment, as the schema describes them. The example of the documentation sends the description and the rules in onedocumentpart instead: it has not been confirmed which of the two FedEx expects. - The JSON parts and the JSON requests are checked before they are sent, as in the other APIs (see Request validation). From one to five files can be uploaded at once, an empty file is refused, and a file name or a media type with a quote or a line break is refused by
FedexClient. Nothing is sent when something is not valid. - The examples of the multipart requests of the schema: the ones of the upload of a document are accepted; the ones of the upload of several documents are not valid JSON (a closing brace is missing, and one of them has a value without a key), and are accepted once fixed. The examples of the two JSON requests are accepted. The minimum sample payloads of the upload of several documents give the metadata of one file in separate entries, and are refused, as the schema says.
- The response is checked against the schema too, and an invalid response throws a
FedexTradeDocumentsExceptionwith the response ingetData(). A field that FedEx adds is accepted. - Never called against the live FedEx API: the schema and the documentation are the only references.
Open ship
FedexOpenShip implements the Open Ship API (schemas/open-ship.json): a shipment that stays open while its packages are added, modified or deleted over several requests, and is confirmed when it is complete, which generates its labels.
use Mariopereira\Fedex\Api\FedexOpenShip; $openShip = new FedexOpenShip($auth); // create the open shipment (this changes data in FedEx) $request = FedexOpenShip::newCreateRequest(); $request['accountNumber']['value'] = '123456789'; $request['index'] = 'Test1234'; // the id of the open shipment in the next requests $request['requestedShipment'] = [/* shipper, recipients, service, packaging, payment, ...: see the schema */]; $openShip->createOpenShipment($request); // add packages $request = FedexOpenShip::newAddPackagesRequest(); $request['accountNumber']['value'] = '123456789'; $request['index'] = 'Test1234'; $request['requestedPackageLineItems'] = [['weight' => ['units' => 'LB', 'value' => 20]]]; $openShip->addPackages($request); // confirm it: FedEx generates the labels $request = FedexOpenShip::newConfirmRequest(); $request['accountNumber']['value'] = '123456789'; $request['index'] = 'Test1234'; $request['labelResponseOptions'] = 'URL_ONLY'; $request['labelSpecification'] = ['labelStockType' => 'PAPER_85X11_TOP_HALF_LABEL', 'imageType' => 'PDF']; $result = $openShip->confirmOpenShipment($request); // a shipment of many packages is confirmed asynchronously: its results come with the job id if (isset($result->output->jobId)) { $result = $openShip->getResultsByJobId('123456789', $result->output->jobId, 'CONFIRM'); } $problems = FedexOpenShip::checkRequest('confirm', $request); // what does not match the whole schema, nothing is sent
| Operation | Request | Method |
|---|---|---|
| Create Open Shipment | newCreateRequest() |
createOpenShipment() |
| Modify Open Shipment | newModifyRequest() |
modifyOpenShipment() |
| Confirm Open Shipment | newConfirmRequest() |
confirmOpenShipment() |
| Modify Open Shipment Packages | newModifyPackageRequest() |
modifyPackage() |
| Add Open Shipment Packages | newAddPackagesRequest() |
addPackages() |
| Delete Open Shipment Packages | newDeletePackagesRequest() |
deletePackages() |
| Retrieve Open Shipment Packages | newRetrievePackageRequest() |
retrievePackage() |
| Delete Open Shipment | newDeleteRequest() |
deleteOpenShipment() and deleteByIndex() |
| Retrieve Open Shipment | newRetrieveRequest() |
retrieveOpenShipment() and retrieveByIndex() |
| Get Open Shipment Results | newResultsRequest() |
getResults() and getResultsByJobId() |
- Creating, modifying, confirming and deleting an open shipment, and adding, modifying and deleting its packages, change real data in FedEx, and confirming generates the labels. The package never repeats those requests, except when FedEx answers that the access token is not valid (nothing was processed then). If an exception is thrown after the request was sent (a timeout, a network failure, or a response that is not valid), the operation may have been done: check it in FedEx before trying again. Only the retrieval of an open shipment, the retrieval of a package and the results change nothing, so only those can be retried (see Retries).
- A request is checked at its top level only, as in Consolidation. FedEx's own collection (
schemas/collections/open-ship.json) contradicts the schema in the nested objects: checked against the whole of the schema, most of its creations and modifications would be refused (they leave outrequestedShipment.labelSpecificationorcustomsClearanceDetail.commercialInvoice, which the schema requires). So the required fields, the types and the allowed values of the top level are checked, and each nested object or list is sent as it comes: FedEx checks the rest and answers with its errors. Nothing is sent when the top level is not valid. 325 of the 327 requests of the collection that can be sent pass this check; the other two leave outindex, which the schema requires (one of them is a negative test). The two sample payloads of the schema pass it too. FedexOpenShip::checkRequest()andcheckResponse()check the whole of the schema and return the problems, each with the path of the field, without stopping anything (nothing is sent). The operation is one ofcreate,modify,confirm,modifyPackage,addPackages,deletePackages,retrievePackage,delete,retrieveandresults. A problem that they report is not necessarily one that FedEx refuses.- The modification and the confirmation are sent to
/ship/v1/openshipments/, with the trailing slash of the schema and of the documentation (the collection leaves it out). - A confirmation of about 40 packages or more is processed asynchronously: the response gives a
jobId, andgetResults()orgetResultsByJobId()(withCONFIRM, orCREATEandMODIFYfor those operations) give the labels or the errors when they are ready. - The response is checked at its top level, and an invalid response throws a
FedexOpenShipExceptionwith the response ingetData(). A field that FedEx adds is accepted. - The schema has two defects, handled like this in the whole check: the retrieval of an open shipment requires a
trackingIdthat it never defines, so it is accepted with any value and is not required; and the values ofalcoholRecipientTypeareLICENSEEandCONSUMER, as in the Ship API (the schema hasCONSUMERstomer). - Never called against the live FedEx API: the schema, the documentation and the collection are the only references.
Request validation
Every API checks its request before sending it, against the schema of FedEx, and sends nothing when the request is not valid. FedEx's own examples contradict its schemas in many places, so the checks of the requests are looser than the schemas, in the same way for every API:
- Types are loose. A text can be given a number or a boolean (
'phoneNumber' => 9013577890,'postalCode' => 75063,'excludeUnavailableLocations' => true), a whole number can be a number with decimals or a text that is a number ('width' => '10.5'), a number can be a text that is a number ('value' => '68.25'), a whole number or a number can be left blank (''), and a boolean can be the text'true'or'false'in any case ('TRUE'), or0or1. The value is sent as it was given, never converted. Everything else is still refused: a text that is not a number ('abc','1,5',' 5'), another text or number for a boolean ('yes',2), a list where a value is expected, a value that is not in the list of allowed values, and a text that does not match the pattern or the length of the schema. - Keys that the schema does not define are sent. FedEx decides about them (a misspelled option of a helper method is not caught either, FedEx answers with an error).
- A few required fields are optional, because the examples of FedEx leave them out: for a shipment
totalWeight,commercialInvoice,commodities,labelSpecification, thelocationIdof a hold at location, thephoneNumberof a contact (except the one of the delivery on invoice) and theallowedSpecialServiceof a return email;includeDetailedScansof tracking by number; thecityand thepostalCodeof an address to validate;codDetailand theweightof a package of the service availability; andcommoditiesandaccountNumberof a rates request. The other required fields of the schema must be present. - The responses are checked as before: a field that FedEx adds is accepted, and a value of the wrong type is not.
FedexConsolidation::checkRequest()also stays strict: it exists to report what does not match the schema.
When FedEx refuses a request that was accepted here, the exception is a FedexClientException, with the errors that FedEx gave.
Parent/child credentials
$auth = new FedexAuth(true, FedexUtils::GRANT_TYPE_PC_CREDENTIALS, 'CLIENT_ID', 'CLIENT_SECRET', 'CHILD_KEY', 'CHILD_SECRET'); // send the child key as child_Key (the default is child_key) $auth->setChildKeyField(FedexAuth::CHILD_KEY_FIELD_SCHEMA);
Timeouts
The API classes and FedexClient take a timeout and a connect timeout, in seconds. The defaults are 30 and 10, which are also the maximum: larger values are lowered to them.
FedexAuth has its own, for the token request:
$auth->setTimeouts(20, 5);
Retries
Nothing is retried by default, apart from a rejected token (401), which is refreshed once. To retry the transient failures, set a retry policy on the FedexClient and pass that client to the API class:
$client = new FedexClient($auth, 'en_US', 30, 10); $client->setRetryPolicy(3, 500, 5000, 30000); // total attempts, base delay, longest delay and longest Retry-After, in milliseconds $locations = new FedexLocationsSearch($auth, 'en_US', 30, 10, $client);
setRetryPolicy() without arguments uses those same values (3 attempts, 500 ms, 5000 ms, 30000 ms). The attempts go from 1 (no retry) to 10.
- A request is retried when FedEx answers 429, 503 or 504, or when it could not be completed (network failure, timeout, ...). Any other failure is thrown at once.
- The wait before the attempt n is a random number of milliseconds between 0 and
min(longest delay, base delay * 2^(n-1)). When the attempts are over, the last failure is thrown as usual. - Only the operations that do not change data in FedEx are retried: postal code validation, locations search, address validation, the availability of a pickup (
FedexPickup::checkPickupAvailability()), the regulatory details ofFedexGlobalTrade, the reprint ofFedexEndOfDayClose, the search ofFedexHarmonizedCode,FedexShip::validate(), the async results ofFedexShip, and the retrievals and the results ofFedexOpenShip.create(),cancel(),createTag()andcancelTag()ofFedexShip,createPickup()andcancelPickup()ofFedexPickup,closeGround()ofFedexEndOfDayClose,createProfiles()andcreateProfile()ofFedexRegulatory, every upload ofFedexTradeDocuments, and the seven operations ofFedexOpenShipthat change data, are never retried, because repeating them could create or cancel a shipment, a tag or a pickup twice, close the shipments again, store the profiles or the documents again, or change an open shipment twice. - When the failure comes with a valid
Retry-Afterheader (a number of seconds, or an HTTP date), the wait is the longer of the header and the backoff, so that a retry is never sent before FedEx asked. If the header asks for more than the fourth argument ofsetRetryPolicy()(30 seconds by default), the retries stop and the failure is thrown, instead of holding the request for that long. With0the header is ignored and only the backoff is used. A header that is not valid is ignored. This is the behaviour of the HTTP standard (RFC 9110): the FedEx schemas that came with the package do not mention the header, so it has not been confirmed that FedEx sends it. - The failure keeps the header, so that your code can use it too:
FedexClientException::getRetryAfter()gives the seconds that FedEx asked to wait, ornull. - To retry your own calls to
FedexClient::request(), passtrueas its last argument, and only for requests that are safe to repeat.
The request for the access token has a retry of its own, set on FedexAuth with the same arguments and the same rules (the client's policy does not apply to it, and it does not retry the failures of the token request):
$auth->setRetryPolicy(3, 500, 5000, 30000);
- It retries a token request that fails with 429, 503 or 504, or that could not be completed. It never retries a request that is refused with 401 (wrong credentials: asking again could lock the account) or any other failure, and it is not used when a valid token comes from the token cache.
- A failure of the token request inside an API request is retried by
FedexAuthonly, so the attempts of the two are not multiplied. Each attempt can take as long as the timeouts, so allow for them inmax_execution_time. FedexAuthException::getRetryAfter()gives theRetry-Afterof the failure, like the exception of the client.
Token cache
By default the access token lives only in the FedexAuth object, so every PHP request asks FedEx for a new one (it is valid for about an hour). To share it between requests, give the authorization a cache:
use Mariopereira\Fedex\Auth\FileTokenCache; $auth->setTokenCache(new FileTokenCache('/var/lib/myapp/fedex-tokens')); // the folder is created if it does not exist
- A token that is still valid is read from the cache instead of being requested, and a token that FedEx gives is stored in it. When FedEx rejects a cached token (401), it is removed and replaced by a new one.
- There is one entry for each environment (sandbox or production) and set of credentials. The key is a SHA-256 digest of them, and the client secret is never stored.
- The files hold the access token, which FedEx accepts until it expires. Keep the folder outside the web root and readable only by the user that runs PHP.
FileTokenCachecreates the folder with mode 0700 and the files with 0600 where the system has file modes; on Windows, protect the folder with its permissions. - A problem of the cache never stops the authorization: an entry that cannot be read or is not valid is ignored, a token that cannot be stored is used anyway, and the token is requested from FedEx. The latest problem is available in
$auth->getLastCacheError()(it never contains the token), so a folder that cannot be written does not go unnoticed. - To keep the token somewhere else (APCu, Redis, a database), implement
FedexTokenCache, which has three methods,get($key),set($key, $data, $ttl)anddelete($key), and pass it tosetTokenCache().set()receives an array of text, numbers andnullthatget()has to return as it is. - Two processes may request a token at the same time, because nothing is locked. Both tokens are valid, and the last one stored is kept.
$auth->setTokenCache(null)stops the use of the cache.
HTTP transport
Requests are sent with cURL by default (CurlTransport). To use another HTTP library, to log the requests, or to test your code without the network, implement FedexTransport and pass it to FedexAuth and FedexClient. Then pass that client to the API class.
use Mariopereira\Fedex\Api\FedexPostalCodeValidation; use Mariopereira\Fedex\Auth\FedexAuth; use Mariopereira\Fedex\Http\FedexClient; use Mariopereira\Fedex\Http\FedexTransport; use Mariopereira\Fedex\Http\FedexTransportResponse; use Mariopereira\Fedex\Misc\FedexUtils; // A transport that answers with fixed data, so that nothing is sent over the network class FakeTransport implements FedexTransport { public function send( string $method, string $url, array $headers, ?string $body, int $timeout, int $connectTimeout ) : FedexTransportResponse { if (false !== strpos($url, '/oauth/token')) { return new FedexTransportResponse(200, '{"access_token":"TEST","token_type":"bearer","expires_in":3600}'); } return new FedexTransportResponse(200, '{"transactionId":"T1","output":{"cleanedPostalCode":"38017"}}'); } } $transport = new FakeTransport(); $auth = new FedexAuth(true, FedexUtils::GRANT_TYPE_CLIENT_CREDENTIALS, 'CLIENT_ID', 'CLIENT_SECRET', null, null, $transport); $client = new FedexClient($auth, 'en_US', 30, 10, $transport); $postal = new FedexPostalCodeValidation($auth, 'en_US', 30, 10, 1, 1, 1, $client); $result = $postal->validate('FDXG', 'US', 'TN', '38017', '2030-01-15'); echo $result->output->cleanedPostalCode;
send() receives the HTTP method, the full url, the header lines ('Name: value', including the Authorization header, so do not log them as they are), the body or null, and the timeouts in seconds. It returns a FedexTransportResponse with the status and the body, and optionally the response headers as a fourth argument (new FedexTransportResponse(503, $body, null, ['Retry-After' => '30']), the names in any case, read with getHeader() and getHeaders()). A transport that does not give the headers works as before, and only what needs one (the Retry-After of a retry) is not available. CurlTransport gives them.
- An HTTP error status (401, 500, ...) is not a failure of the transport: return it with its body, and the package turns it into the exceptions described below.
- A request that could not be completed (network failure, timeout, ...) is returned as
new FedexTransportResponse(0, '', 'the error text'). The transport must not throw. - When a client is passed to an API class, the locale and timeouts given to the API class are not used: set them on the client.
Errors
Every exception thrown by the package extends FedexException: FedexAuthException, FedexClientException, FedexPostalCodeValidationException, FedexLocationsSearchException and FedexAddressValidationException.
They provide getStatusCode(), getTransactionId(), getCustomerTransactionId(), getErrors(), getErrorsAsString() and getData(). The access token is removed from the data kept in the exceptions.
getCode() returns one of these constants:
| Constant | Value | Meaning |
|---|---|---|
FedexException::EC_MISSING_PARAMETER |
1 | A required parameter is missing |
FedexException::EC_BAD_PARAMETER |
2 | A parameter has an invalid value |
FedexException::EC_TRANSPORT_ERROR |
100 | The request failed: a network failure (the message starts with Transport error: and the status is 0) or an HTTP error status |
FedexException::EC_BAD_RESULT_RETURNED |
101 | The response received is not valid |
FedexException::EC_UNABLE_TO_AUTHENTICATE |
200 | Authentication failed: no access token could be obtained, or HTTP status 401 |
Development
schemas/holds the FedEx OpenAPI specifications that the code is checked against.- Install with
composer installand run the tests withcomposer test. They use PHPUnit 9.6, the last version that runs on PHP 7.4, so run them with the oldest PHP that the package supports. No test uses the network except the loopback tests, which talk to a throwaway server on127.0.0.1only, and no test calls FedEx. - The API documentation is generated with PhpDocumentor (PHP 8 or newer):
vendor/bin/phpdoc -d src -t docs. - CONTRIBUTING.md has the rest: the conventions, the test groups (
slowandloopback), the fast run, how to rebuild the documentation, and what a release contains.
License
GPL-3.0-or-later. See the LICENSE file.