Developer notes
Technical reference for developers who customize FedEx REST Rates & Labels.
Add-on identity
Section titled “Add-on identity”| Item | Value |
|---|---|
| Add-on id | kv_fedex_rest |
| Function prefix | fn_kv_fedex_rest_ |
| Language variables | kv_fedex_rest.* |
| CSS scope | .kv-fedex-rest |
| PHP namespace | Tygh\Addons\KvFedexRest |
| Shipping service module | kv_fedex_rest (class Tygh\Shippings\Services\KvFedexRest); the core REST module is fedexrest, no overlap |
FedEx endpoints used and where each is documented
Section titled “FedEx endpoints used and where each is documented”Hosts: https://apis-sandbox.fedex.com (test) and https://apis.fedex.com (live). Every call except the token carries Authorization: Bearer <token>, Accept: application/json, X-locale: en_US. Sources are the FedEx Developer Portal API references (developer.fedex.com). Status:
- official-spec: checked field by field against the OpenAPI document the portal publishes (
developer.fedex.com/wirc/json/api_groups/<Group>/<Resource>.json, fetched 2026-11-07; the sample requests and the response examples are intests/fixtures/). - portal-reference: written from the portal’s reference page and guide text and from the request body of CS-Cart’s own core Rate integration; the OpenAPI file of this group is not published, so the field names must still be confirmed in the sandbox. Flagged items are repeated under “Open items”.
| Purpose | Endpoint | Fields we send / read | Status |
|---|---|---|---|
| OAuth token | POST /oauth/token, application/x-www-form-urlencoded: grant_type=client_credentials, client_id, client_secret (child key accounts: grant_type=csp_credentials, child_key, child_secret) |
reads access_token, expires_in (token cached encrypted; one new login on HTTP 401) |
portal-reference (the core module uses the same form; csp_credentials is from the portal’s integrator guide, flagged) |
| Rates | POST /rate/v1/rates/quotes |
accountNumber.value, rateRequestControlParameters.returnTransitTimes, requestedShipment.{shipper.address, recipient.address(+residential), pickupType, packagingType, rateRequestType[], preferredCurrency, requestedPackageLineItems[].{weight, dimensions, declaredValue, packageSpecialServices}}; no serviceType (all services in one answer). Reads output.rateReplyDetails[].{serviceType, serviceName, ratedShipmentDetails[].{rateType, totalNetCharge, currency}, operationalDetail.{deliveryDate, transitTime}} |
portal-reference (endpoint and body match the core Fedexrest class; returnTransitTimes, operationalDetail.deliveryDate flagged) |
| Create shipment (label) | POST /ship/v1/shipments |
labelResponseOptions (LABEL), accountNumber.value, requestedShipment.{shipDatestamp, pickupType, serviceType, packagingType, shipper, recipients[], shippingChargesPayment{paymentType, payor.responsibleParty.accountNumber}, labelSpecification{labelFormatType, imageType, labelStockType}, rateRequestType, preferredCurrency, totalPackageCount, requestedPackageLineItems[]{sequenceNumber, weight, dimensions, declaredValue, customerReferences, packageSpecialServices}, shipmentSpecialServices (returns), customsClearanceDetail, shippingDocumentSpecification}; header x-customer-transaction-id. Reads output.transactionShipments[0].{masterTrackingNumber, serviceType, pieceResponses[].{trackingNumber, netChargeAmount, packageDocuments[].{docType, encodedLabel}}, shipmentDocuments[], completedShipmentDetail} |
official-spec |
| Return label | same endpoint with shipmentSpecialServices.specialServiceTypes=["RETURN_SHIPMENT"], returnShipmentDetail.returnType="PRINT_RETURN_LABEL" |
official-spec | |
| Cancel shipment | PUT /ship/v1/shipments/cancel |
accountNumber.value, trackingNumber, senderCountryCode, deletionControl="DELETE_ALL_PACKAGES"; reads output.cancelledShipment |
official-spec |
| Track | POST /track/v1/trackingnumbers |
includeDetailedScans, trackingInfo[].trackingNumberInfo.trackingNumber; reads output.completeTrackResults[0].trackResults[0].{latestStatusDetail.{code, derivedCode, statusByLocale}, scanEvents[].{date, eventDescription, scanLocation}, error} |
portal-reference (status codes DL, IT, OC, DE, SE, CA, others = in transit; flagged) |
| Address validation | POST /address/v1/addresses/resolve |
inEffectAsOfTimestamp, validateAddressControlParameters.includeResolutionTokens, addressesToValidate[].address; reads output.resolvedAddresses[0].{streetLinesToken, city, stateOrProvinceCode, postalCode, countryCode, classification, customerMessages, attributes} |
portal-reference (flagged) |
| Pickup availability | POST /pickup/v1/pickups/availabilities |
pickupAddress, pickupRequestType[], carriers[], countryRelationship, dispatchDate, packageReadyTime, customerCloseTime; reads output.options[] |
official-spec |
| Create pickup | POST /pickup/v1/pickups |
associatedAccountNumber.value, associatedAccountNumberType, originDetail.{pickupLocation, packageLocation, readyDateTimestamp, customerCloseTime}, carrierCode (FDXE / FDXG), countryRelationships, packageCount, totalWeight, remarks; reads output.{pickupConfirmationCode, location} |
official-spec (the sample’s readyDateTimestamp ends in Z; the zone semantics of the time are flagged) |
| Cancel pickup | PUT /pickup/v1/pickups/cancel |
associatedAccountNumber.value, pickupConfirmationCode, carrierCode, scheduledDate, location (Express) |
official-spec |
| Error body | HTTP 4xx / 5xx: {"transactionId", "errors": [{"code", "message"}]} |
message and code shown (masked) in the log and to the admin | official-spec |
The Ship API has no idempotency key and no “find my shipment by reference” call. That is why a purchase is protected on our side (see “Safe label purchase”).
Open items to confirm in the FedEx sandbox
Section titled “Open items to confirm in the FedEx sandbox”- Rate:
rateRequestControlParameters.returnTransitTimesandoperationalDetail.deliveryDate/transitTime. - OAuth:
grant_type=csp_credentialsfields for child keys (only used when a child key is entered). - Track: status code vocabulary,
scanEvents[]shape. - Address validation: response field names and the
classificationvalues. - Pickup: whether
readyDateTimestampis read as UTC or as the pickup location’s local time. The add-on sends the store-local wall-clock time with a trailingZ, like the official sample. - Brand: the FedEx logo may only be used under FedEx’s brand rules (permission); the add-on and its gallery use the name as text only.
Structure
Section titled “Structure”| Class | Role |
|---|---|
Client\BaseClient |
HTTP (cURL, TLS verification, timeouts), JSON, OAuth2 token cache (encrypted), one re-login on 401, error mapping, masked logging |
Client\Api |
The FedEx calls above |
Client |
Client per company, the connection test |
Accounts, Secret |
Credentials (test and live) sealed with AES-256-GCM; one account per store owner or per vendor; non-secret fields (account number, ship-from, defaults) in extra |
Config, Access |
Settings, owner mode, who may see and manage what (Access::canLabel() guards every money action) |
RateCalculator |
Rate request, rate list parse, markup, free shipping, fallback; memo and 10 minute cache per request |
Shipments |
Payload (outbound, return, customs), multi-package, safe purchase, cancel, tracking, downloads |
Pickups, Addresses |
Pickup API, Address Validation API |
Parties, Packer, Clock |
Address and party mapping (multi-byte safe cuts), units and package split, shop time zone dates |
Queue, Jobs, JobException, Log |
Job queue (back-off 5 min / 15 min / 1 h / 3 h / 12 h), recurring work, event log with masking |
Safe label purchase
Section titled “Safe label purchase”- A MySQL named lock per order (
kv_fedex_rest_order_<id>) serialises create, return and cancel; tracking uses a per-shipment lock. - Before the call a row with status
Qand arequest_key(hash of order, kind, service and packages, not the ship date) is written; the same request finds it. A finished one is returned, a running one makes the job wait, a stale or unresolved one stops with a message. - A timeout, a connection error or HTTP 408 / 500 / 502 / 504 turns the row into
U(unresolved) and nothing retries automatically. HTTP 400 / 401 / 403 / 404 / 429 / 503 delete the row because FedEx did not process the request. Thex-customer-transaction-idheader (kv-<order>-<shipment>) is echoed by FedEx and helps to find the shipment in the FedEx logs. - Pickups use the same pattern (
request_key,Ustatus). - The named lock needs MySQL 5.7+ / MariaDB 10.0.2+ to coexist with the queue’s own lock (older servers release the first lock when another is taken; purchases stay protected by the attempt rows).
Database tables
Section titled “Database tables”| Table | Purpose |
|---|---|
?:kv_fedex_rest_accounts |
Credentials per company (secrets sealed), last connection check, token cache |
?:kv_fedex_rest_log |
Log |
?:kv_fedex_rest_jobs |
Queue |
?:kv_fedex_rest_shipments |
Shipments: kind (outbound / return), service, status, tracking, scan events |
?:kv_fedex_rest_pieces |
One row per package label and per document (commercial invoice), file data |
?:kv_fedex_rest_pickups |
Pickups |
?:kv_fedex_rest_addr |
Address validation result per order and address hash |
Hooks used
Section titled “Hooks used”PHP: change_order_status_post (label at a status), delete_order (removes shipments, labels and the address check), init_templater_post (upgrade synchronizer, only while pending). Template hooks: orders:tabs_extra (admin tab), addons:manage / addons:tabs_content (licence card). Controllers: kv_fedex_rest (admin, vendor, storefront cron), orders.post (tab data). No storefront output apart from the shipping rates, so there is nothing to cache or exclude from the full page cache; rates are only requested while a cart is calculated.
Testing against a mock
Section titled “Testing against a mock”Define the base URL in config.local.php to send all FedEx calls to another host, for example the mock in tests/mock-server/server.js:
$config['kv_fedex_rest_base_url'] = 'http://127.0.0.1:11830';Extending
Section titled “Extending”The service list is Installer::services() (code to name; codes are the FedEx serviceType values). Add a code there and in fn_kv_fedex_rest_upgrade_sync() (it re-runs Installer::install()).