Skip to content

Developer notes

Technical reference for developers who customize FedEx REST Rates & Labels.

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 in tests/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”
  1. Rate: rateRequestControlParameters.returnTransitTimes and operationalDetail.deliveryDate / transitTime.
  2. OAuth: grant_type=csp_credentials fields for child keys (only used when a child key is entered).
  3. Track: status code vocabulary, scanEvents[] shape.
  4. Address validation: response field names and the classification values.
  5. Pickup: whether readyDateTimestamp is read as UTC or as the pickup location’s local time. The add-on sends the store-local wall-clock time with a trailing Z, like the official sample.
  6. 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.
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
  • 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 Q and a request_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. The x-customer-transaction-id header (kv-<order>-<shipment>) is echoed by FedEx and helps to find the shipment in the FedEx logs.
  • Pickups use the same pattern (request_key, U status).
  • 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).
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

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.

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';

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()).