UPS is the other global express carrier an Indian seller will meet alongside FedEx and DHL, mostly on the export lane and on inbound samples from suppliers. Like every carrier in this catalogue it is not a sales channel: there are no orders, listings or stock behind the API. What UPS gives us is a rate and transit time quote before we book, a 1Z tracking number and a label when we book, a pickup request number, a scan by scan activity history, signature and photo proof of delivery, and, unusually for a carrier, a real HTTP webhook through the Track Alert API. Everything below refers to the REST APIs on the UPS Developer Portal, verified against the OpenAPI specifications and sample request and response files that UPS itself publishes on GitHub.
At a glance
developer.ups.com timed out and ups.com/upsdeveloperkit returned Access Denied to an unauthenticated fetcher on 2026-09-21. Every endpoint path, parameter, enum value and sample body on this page therefore comes from two first party sources that are reachable: the UPS OpenAPI specification repository at github.com/UPS-API/api-documentation and the UPS sample application repository at github.com/UPS-API/java-api-examples, whose src/main/resources folders hold the request and response JSON files UPS ships with its own examples. Claims about onboarding, pricing and quotas are marked where they could not be verified.
What it is
United Parcel Service is a United States listed carrier running a global small parcel network plus UPS Supply Chain Solutions for freight forwarding and contract logistics. For a seller database the relevant services are UPS Ground and Ground Saver in North America, UPS Worldwide Express, Expedited, Saver and Economy on international lanes, and UPS Mail Innovations where the last mile is handed to a postal operator.
In India, UPS operates through a joint venture and is used mainly for outbound international parcels and for inbound documents. It is not a domestic mass market courier in the way Delhivery or Blue Dart are, so for most Indian D2C brands a UPS connection covers export orders and supplier inbound rather than the daily domestic volume. As with FedEx, one connection is one UPS account number, and the account number is what most of the API treats as the tenant key.
API access
The Developer Portal issues credentials per application:
- Create an account on developer.ups.com and add your UPS account number to the profile.
- Create an app. The app gets a client id and a client secret. You choose whether the app uses the client credentials flow (you are the shipper) or the authorization code flow (you are a platform shipping on behalf of other UPS account holders).
- Apps work against the Customer Integration Environment at
https://wwwcie.ups.comfirst, then against production athttps://onlinetools.ups.com. The paths, headers and bodies are identical, so promotion is a base URL and credential swap.
APIs are versioned in the path. The current defaults declared in the UPS specifications on 2026-09-21 are v2409 for Shipping, Rating and Pickup Creation, v1 for Tracking and Time in Transit, v3 for Quantum View and v2 for Track Alert. The version segment is how UPS lets a caller opt in to new response fields, so pin it explicitly rather than relying on a default.
UPS publishes SDKs for the OAuth flows at github.com/UPS-API/UPS-SDKs, Postman collections linked from every specification, a set of runnable Java examples, and an MCP server at github.com/UPS-API/ups-mcp. The OpenAPI specifications themselves are in github.com/UPS-API/api-documentation, one YAML file per API, which is the most reliable machine readable source for this connector.
The legacy UPS Developer Kit is a separate and older product: XML or SOAP posted to https://onlinetools.ups.com/ups.app/xml/ShipConfirm, .../ShipAccept, .../Rate and similar, authenticated with an Access License Number plus a UPS username and password in an AccessRequest envelope. UPS has moved all new development to the OAuth secured REST APIs described here, and its published specification repository contains REST specifications only. The exact retirement date for the XML endpoints and for access key authentication could not be verified on 2026-09-21 because both developer.ups.com and ups.com blocked the fetcher, so confirm it on the portal before relying on a legacy integration. Do not build new work on the XML endpoints, and do not mix the credential models: the REST APIs never take an Access License Number.
Authentication
Two flows, one token format. Both return a bearer token you put in Authorization: Bearer <token>.
Client credentials, when we are the shipper
- Base64 encode
client_id:client_secretand send it as HTTP Basic. POST https://onlinetools.ups.com/security/v1/oauth/tokenwithContent-Type: application/x-www-form-urlencodedand bodygrant_type=client_credentials.- Optionally send
x-merchant-id, described in the UPS specification as the 6 digit UPS account number. Send it when one app serves several UPS accounts. - Read
access_tokenandexpires_infrom the response.expires_inis a string of seconds. - Re-mint when it expires. There is no refresh token on this flow.
Note the host: the token endpoint sits at the root, not under /api. Every business API sits under /api.
curl -X POST "https://wwwcie.ups.com/security/v1/oauth/token" \ -u "MY_CLIENT_ID:MY_CLIENT_SECRET" \ -H "Content-Type: application/x-www-form-urlencoded" \ -H "x-merchant-id: A1B2C3" \ -d "grant_type=client_credentials"
{
"token_type": "Bearer",
"issued_at": "1758412800000",
"client_id": "MY_CLIENT_ID",
"access_token": "eyJraWQiOiI2NGI0...",
"scope": "",
"expires_in": "14399",
"refresh_count": "0",
"status": "approved"
}
The response fields are exactly token_type, issued_at, client_id, access_token, scope, expires_in, refresh_count and status, per the UPS tokenSuccessResponse schema. The documented error statuses on this endpoint are 400 Invalid Request, 401 Unauthorized, 403 Blocked Merchant and 429 Quota Limit Exceeded.
Authorization code, when a merchant authorises us
This is the multi account model, and it is the one to build if we onboard merchants who keep their own UPS accounts.
- Redirect the merchant to
GET https://onlinetools.ups.com/security/v1/oauth/authorizewith the requiredclient_id,redirect_uriandresponse_type, plus optionalstate,scopeandcode_challengefor PKCE. - The merchant signs in to UPS and consents; UPS redirects back to
redirect_uriwith acode. POST https://onlinetools.ups.com/security/v1/oauth/tokenwithgrant_type=authorization_code,code,redirect_uri,client_idand, if PKCE was used,code_verifier.- The response adds
refresh_token,refresh_token_issued_at,refresh_token_expires_in,refresh_token_statusandrefresh_countto the same field set as above. POST https://onlinetools.ups.com/security/v1/oauth/refreshwithgrant_type=refresh_tokenandrefresh_tokento roll it forward.
Store one credential row per merchant with the refresh token, the refresh token expiry and the UPS account number, and run a refresh job well before refresh_token_expires_in elapses. The precise token and refresh token lifetimes are returned per response rather than fixed in the specification, so read them rather than hard coding.
Authenticated call
Almost every UPS business endpoint takes two extra required headers, transId (a unique id for this request) and transactionSrc (a string identifying the calling application). They come back in Response.TransactionReference so they are the correlation key for support tickets.
curl -X GET \ "https://onlinetools.ups.com/api/track/v1/details/1Z023E2X0214323462?locale=en_US&returnSignature=false&returnPOD=false" \ -H "Authorization: Bearer eyJraWQiOiI2NGI0..." \ -H "transId: 4f2e7f1c-8f1f-4c2c-9f2a-5d9b7a3c1e01" \ -H "transactionSrc: acme-commerce-db"
Objects we can read
UPS is a carrier. Of the unified model objects only shipments is natively present, with returns partially present through return services. There are no orders, order items, products, listings, inventory, customers or settlements.
Orders
Not available. UPS has no order object. Order context reaches UPS only as reference numbers: Shipment.ReferenceNumber[] and Package.ReferenceNumber[], each { Code, Value, BarCodeIndicator }, written at ship time. Those same values are then queryable through GET /api/track/v1/reference/details/{referenceNumber}, so writing our order_id into a reference number is what makes order to shipment reconciliation possible later.
Shipments and tracking
Three read endpoints, all GET, all under /api:
Parameters on /track/v1/details/{inquiryNumber}:
inquiryNumberin the path, 7 to 34 characters.localequery, defaulten_US.returnSignaturequery, defaultfalse.truereturns the delivery signature image as base64 bytes.returnPODquery, defaultfalse. Returns the proof of delivery document.returnMilestonesquery, defaultfalse. Returns the coarse milestone list rather than only raw scans.transIdandtransactionSrcheaders, both required.
GET /track/v1/shipment/details/{inquiryNumber} is the only paged endpoint in this set: it takes offset and count query parameters. The others return everything they have.
UPS states in the Track API specification that tracking data "is rolled off after the 120 day retention period and may not be returned in the response after the retention period". Our database has to be the system of record for anything older than 120 days: back fill on receipt, never plan to re-read history.
Response, trimmed to one package and one activity, using the field names and example values from the UPS Track specification. Addresses here are PII.
{
"trackResponse": {
"shipment": [
{
"inquiryNumber": "1Z023E2X0214323462",
"package": [
{
"trackingNumber": "1Z023E2X0214323462",
"currentStatus": { "code": "SR", "statusCode": "003", "type": "X", "description": "Released by the customs agency.", "simplifiedTextDescription": "Delivered" },
"activity": [
{
"date": "20210210",
"time": "071356",
"gmtDate": "20210210",
"gmtTime": "74700",
"gmtOffset": "-05:00",
"location": {
"slic": "8566",
"address": { "addressLine1": "100 Main St", "city": "Wayne", "stateProvince": "NJ", "postalCode": "07470", "countryCode": "US" }
},
"status": { "code": "SR", "statusCode": "003", "type": "X", "description": "Your package was released by the customs agency." }
}
],
"deliveryDate": [ { "type": "DEL", "date": "20210212" } ],
"deliveryTime": { "type": "DEL", "startTime": "", "endTime": "143200" },
"deliveryInformation": {
"location": "Front Door",
"receivedBy": "",
"signature": { "image": "encoding Base64" },
"deliveryPhoto": { "photo": "", "photoCaptureInd": "", "photoDispositionCode": "" },
"pod": { "content": "" }
},
"packageAddress": [ { "type": "ORIGIN/DESTINATION", "name": "Sears", "address": {} } ],
"referenceNumber": [ { "type": "SHIPMENT", "number": "ShipRef123" } ],
"alternateTrackingNumber": [ { "type": "USPS_PIC", "number": "92419900000033499522966220" } ],
"paymentInformation": [
{ "type": "ICOD/COD", "amount": "243.5", "currency": "EUR", "paid": false, "paymentMethod": "C0", "id": "3S35571M1L381K5O0P316L0M1R2E6H14" }
],
"service": { "code": "518", "levelCode": "011", "description": "UPS Ground" },
"weight": { "unitOfMeasurement": "LBS", "weight": "10.0" },
"packageCount": 2,
"milestones": [ { "code": "", "category": "", "state": "", "current": true } ]
}
]
}
]
}
}
Notes for the ingest:
currentStatus.typeis the coarse bucket andcurrentStatus.codethe fine grained scan code. Map on the codes, not ondescriptionorsimplifiedTextDescription, which change withlocale.dateandtimeare local strings inYYYYMMDDandHHMMSSform with no separators.gmtDate,gmtTimeandgmtOffsetare the normalised trio, so build the timestamp from those.paymentInformation[]withtypeofICOD/CODis where a cash on delivery amount and itspaidflag appear. That is the closest UPS gets to COD reconciliation on the read side.alternateTrackingNumber[]matters for Mail Innovations and Ground Saver, where the last mile carries a USPS identifier and a customer who searches by that number must still find our row.- A package that is genuinely unknown comes back as
404with anerrors[]array of{ code, message }, not as an empty success.
Proof of delivery
returnSignature=true puts a base64 signature image at deliveryInformation.signature.image, returnPOD=true populates deliveryInformation.pod.content, and residential deliveries may carry deliveryInformation.deliveryPhoto. All three are per request flags on the same Track endpoint rather than separate endpoints, and all three are PII or near PII, so store them deliberately.
Rates, serviceability and transit time
POST /api/rating/{version}/{requestoption} answers serviceability, price and, on the right request option, transit time. The requestoption path segment selects the behaviour:
There is no separate postal code serviceability endpoint on this API. Serviceability is a Shop call that either returns services or does not. POST /api/shipments/{version}/transittimes in the Time in Transit API answers the delivery date question on its own when no price is needed.
Official UPS sample request, from the UPS Java examples repository, trimmed:
{
"RateRequest": {
"Request": {
"RequestOption": "Rate",
"SubVersion": "2108",
"TransactionReference": { "CustomerContext": "CustomerContext" }
},
"Shipment": {
"Shipper": {
"Name": "ShipperName",
"ShipperNumber": "A1B2C3",
"Address": { "AddressLine": "123 main street", "City": "TIMONIUM", "StateProvinceCode": "MD", "PostalCode": "21093", "CountryCode": "US" }
},
"ShipTo": {
"Name": "ShipToName",
"Address": { "AddressLine": "ShipToAddressLine", "City": "Alpharetta", "StateProvinceCode": "GA", "PostalCode": "30005", "CountryCode": "US" }
},
"PaymentDetails": { "ShipmentCharge": { "Type": "01", "BillShipper": { "AccountNumber": "A1B2C3" } } },
"Service": { "Code": "03", "Description": "Ground" },
"NumOfPieces": "1",
"Package": {
"PackagingType": { "Code": "02", "Description": "Packaging" },
"Dimensions": { "UnitOfMeasurement": { "Code": "IN" }, "Length": "5", "Width": "5", "Height": "5" },
"PackageWeight": { "UnitOfMeasurement": { "Code": "LBS" }, "Weight": "1" }
}
}
}
}
Official UPS sample response, trimmed:
{
"RateResponse": {
"Response": {
"ResponseStatus": { "Code": "1", "Description": "Success" },
"Alert": [ { "Code": "110971", "Description": "Your invoice may vary from the displayed reference rates" } ],
"TransactionReference": { "CustomerContext": "testing" }
},
"RatedShipment": {
"Service": { "Code": "03", "Description": "" },
"BillingWeight": { "UnitOfMeasurement": { "Code": "LBS", "Description": "Pounds" }, "Weight": "1.0" },
"TransportationCharges": { "CurrencyCode": "USD", "MonetaryValue": "12.49" },
"BaseServiceCharge": { "CurrencyCode": "USD", "MonetaryValue": "0.00" },
"ServiceOptionsCharges": { "CurrencyCode": "USD", "MonetaryValue": "0.00" },
"TotalCharges": { "CurrencyCode": "USD", "MonetaryValue": "12.49" },
"RatedPackage": {
"TransportationCharges": { "CurrencyCode": "USD", "MonetaryValue": "12.49" },
"BaseServiceCharge": { "CurrencyCode": "USD", "MonetaryValue": "10.65" },
"ItemizedCharges": [ { "Code": "376", "CurrencyCode": "USD", "MonetaryValue": "0.00", "SubType": "Suburban" } ],
"TotalCharges": { "CurrencyCode": "USD", "MonetaryValue": "12.49" },
"Weight": "1.0"
},
"TimeInTransit": null
}
}
}
Note the shape trap, and it bites every UPS integration once: UPS serialises a single element collection as an object, and a multi element collection as an array. RatedShipment, RatedPackage, Package, PackageResults, Disclaimer and ShipmentCharge all do this. A Shop request returns RatedShipment as an array, a Rate request returns it as an object. Normalise every one of these to a list on the way in.
Negotiated rates only appear when Shipment.ShipmentRatingOptions.NegotiatedRatesIndicator is set on the request, and then arrive under NegotiatedRateCharges. Published rates arrive under TotalCharges. Capture both if we want to show a merchant their discount.
Pickup
pickuptype takes oncall, smart or both. CancelBy takes 01 for account number or 02 for PRN, and the PRN goes in the Prn header. The pending status call takes the account number in an AccountNumber header.
Official UPS sample request, trimmed:
{
"PickupCreationRequest": {
"Request": { "TransactionReference": { "CustomerContext": "shipper account payment method" } },
"RatePickupIndicator": "N",
"Shipper": { "Account": { "AccountNumber": "A1B2C3", "AccountCountryCode": "US" } },
"PickupDateInfo": { "PickupDate": "20221130", "ReadyTime": "0500", "CloseTime": "1400" },
"PickupAddress": {
"CompanyName": "Pickup Proxy",
"ContactName": "Pickup Manager",
"AddressLine": "Pickup Address",
"Room": "R01",
"Floor": "2",
"City": "Allendale",
"StateProvince": "NJ",
"PostalCode": "07401",
"CountryCode": "US",
"ResidentialIndicator": "Y",
"PickupPoint": "Lobby",
"Phone": { "Number": "123456789", "Extension": "1234" }
},
"AlternateAddressIndicator": "Y",
"PickupPiece": [ { "ServiceCode": "001", "Quantity": "27", "DestinationCountryCode": "US", "ContainerCode": "01" } ],
"TotalWeight": { "Weight": "5.5", "UnitOfMeasurement": "LBS" },
"OverweightIndicator": "N",
"PaymentMethod": "01",
"SpecialInstruction": "Ring the bell at the loading dock",
"ReferenceNumber": "OUR-ORDER-REF",
"Notification": { "ConfirmationEmailAddress": "ops@example.com", "UndeliverableEmailAddress": "ops@example.com" }
}
}
The response is PickupCreationResponse with Response.ResponseStatus, a PRN (the Pickup Request Number, which is the handle for cancellation), WeekendServiceTerritory, RateStatus and, when RatePickupIndicator is Y, a RateResult with ChargeDetail[], TotalTax and GrandTotalOfAllCharge. Store the PRN against the shipment.
Address validation
POST /api/addressvalidation/{version}/{requestoption} where requestoption is an integer: 1 validate, 2 classify residential against commercial, 3 both. Query parameters regionalrequestindicator and maximumcandidatelistsize control how many candidate addresses come back.
{
"XAVRequest": {
"AddressKeyFormat": {
"ConsigneeName": "UPS",
"AttentionName": "UPS",
"AddressLine": "123 Main St",
"PoliticalDivision2": "Roswell",
"PoliticalDivision1": "GA",
"PostcodePrimaryLow": "30076",
"PostcodeExtendedLow": "1521",
"CountryCode": "US"
}
}
}
PoliticalDivision2 is the city and PoliticalDivision1 the state or province, which is the one piece of UPS naming that reliably confuses new integrators.
Quantum View
POST /api/quantumview/{version}/events (default v3) is an account level activity feed rather than a per tracking number lookup. You ask for a subscription by name or by a date and time range and UPS returns the manifest, origin, exception and delivery events that landed in that window for the whole account.
{
"QuantumViewRequest": {
"Request": {
"RequestAction": "QVEvents",
"TransactionReference": { "CustomerContext": "nightly sweep" }
},
"SubscriptionRequest": {
"DateTimeRange": { "BeginDateTime": "20221215000000", "EndDateTime": "20221220000000" }
}
}
}
This is the right endpoint for a nightly catch up sweep that guarantees we did not miss a webhook, because it is keyed on time rather than on a list of tracking numbers we already know about.
Returns and cancellations
Partial. UPS models returns as label variants rather than as a return object with a reason and a refund. Shipment.ReturnService on the Ship request produces a return label, print return label or electronic return label depending on the service code, and ShipmentServiceOptions carries the related flags such as ReturnOfDocumentIndicator and ExchangeForwardIndicator. There is also a Delivery Intercept API (/api/DeliveryIntercept/return/{tracking_number}, .../redirect/address/{tracking_number}, .../reschedule/{tracking_number}, .../willcall/{tracking_number}, .../cancel/{tracking_number}) which turns an in flight parcel around, which is the nearest thing to an RTO trigger.
There is no NDR object and no RTO object. A failed attempt is a scan with an exception status.type and the reason in status.description; an RTO is a new tracking number moving in the reverse direction. Both have to be derived from the activity stream.
Payments and settlements
Not available in these APIs. UPS billing and invoice data is a separate product. Cash on delivery is configured on the way out through ShipmentServiceOptions.COD and AccessPointCOD, and the collected amount and its paid flag show up on the read side in paymentInformation[] on a track result. That is useful signal but it is not a remittance file: there is no COD remittance API here, so money received has to be reconciled from UPS billing files outside this catalogue.
Customers, products, listings, inventory, locations
Not applicable. The only location concept is the UPS network itself: the Locator API (POST /api/locations/{version}/search/availabilities/{reqOption}) finds UPS Access Points and drop off locations. Those are not seller warehouses.
Writing back: listings, price and stock
Not applicable, this is a carrier. There are no listings, prices or stock levels at UPS and nothing here writes to a sales channel. The write path is shipment lifecycle.
Create a shipment
POST /api/shipments/{version}/ship. Request.RequestOption takes validate or nonvalidate and decides whether UPS hard fails on an address it cannot resolve. There is also an additionaladdressvalidation query parameter.
Official UPS sample request, trimmed:
{
"ShipmentRequest": {
"Request": {
"SubVersion": "1801",
"RequestOption": "nonvalidate",
"TransactionReference": { "CustomerContext": "our-order-1206" }
},
"Shipment": {
"Description": "1206 PTR",
"Shipper": {
"Name": "US MEDI/HOME CARE",
"AttentionName": "MAQUOKETA MEDICARE",
"TaxIdentificationNumber": "456789",
"Phone": { "Number": "1234567890" },
"ShipperNumber": "A1B2C3",
"Address": { "AddressLine": "ShipperAddress", "City": "Albany", "StateProvinceCode": "NY", "PostalCode": "12211", "CountryCode": "US" }
},
"ShipTo": {
"Name": "TEST USER",
"Phone": { "Number": "1234567890" },
"Address": { "AddressLine": "ShipToAddress", "City": "San Jose", "StateProvinceCode": "CA", "PostalCode": "95113", "CountryCode": "US" }
},
"ShipFrom": {
"Name": "ShipFromName",
"Address": { "AddressLine": "ShipFromAddress", "City": "Albany", "StateProvinceCode": "NY", "PostalCode": "12211", "CountryCode": "US" }
},
"PaymentInformation": { "ShipmentCharge": { "Type": "01", "BillShipper": { "AccountNumber": "A1B2C3" } } },
"Service": { "Code": "01", "Description": "Expedited" },
"Package": {
"Description": "International Goods",
"Packaging": { "Code": "02" },
"PackageWeight": { "UnitOfMeasurement": { "Code": "LBS" }, "Weight": "10" }
},
"ShipmentRatingOptions": { "NegotiatedRatesIndicator": "" }
},
"LabelSpecification": { "LabelImageFormat": { "Code": "GIF" } }
}
}
Official UPS sample response, trimmed, with the label bytes cut:
{
"ShipmentResponse": {
"Response": {
"ResponseStatus": { "Code": "1", "Description": "Success" },
"TransactionReference": { "CustomerContext": "GG", "TransactionIdentifier": "trhel8vm1dh3CZG6Xq6QKT" }
},
"ShipmentResults": {
"Disclaimer": { "Code": "05", "Description": "Rate excludes VAT. Rate includes a fuel surcharge." },
"ShipmentCharges": {
"TransportationCharges": { "CurrencyCode": "USD", "MonetaryValue": "193.64" },
"ServiceOptionsCharges": { "CurrencyCode": "USD", "MonetaryValue": "0.00" },
"TotalCharges": { "CurrencyCode": "USD", "MonetaryValue": "193.64" }
},
"NegotiatedRateCharges": { "TotalCharge": { "CurrencyCode": "USD", "MonetaryValue": "198.42" } },
"RatingMethod": "02",
"BillableWeightCalculationMethod": "02",
"BillingWeight": { "UnitOfMeasurement": { "Code": "LBS", "Description": "Pounds" }, "Weight": "10.0" },
"ShipmentIdentificationNumber": "1ZWA82900123277535",
"PackageResults": {
"TrackingNumber": "1ZWA82900123277535",
"BaseServiceCharge": { "CurrencyCode": "USD", "MonetaryValue": "170.23" },
"ServiceOptionsCharges": { "CurrencyCode": "USD", "MonetaryValue": "0.00" },
"ShippingLabel": {
"ImageFormat": { "Code": "GIF", "Description": "GIF" },
"GraphicImage": "R0lGODlheAUgA/cAAAAAAAEBAQICAgMDAwQEBAUF...",
"HTMLImage": "PCFET0NUWVBFIEhUTUwgUFVCTElDICItLy9JRVRG..."
},
"ItemizedCharges": [ { "Code": "376", "CurrencyCode": "USD", "MonetaryValue": "0.00", "SubType": "Urban" } ]
}
}
}
}
ShipmentIdentificationNumber is the shipment level 1Z number and PackageResults[].TrackingNumber the per package one. On a single package shipment they are the same value, which is why single package integrations often persist only one and then break on the first multi piece shipment. Persist both. ShippingLabel.GraphicImage is base64 of the label in the requested LabelImageFormat, typically GIF, ZPL or EPL, and HTMLImage is a printable wrapper.
Recover a label
POST /api/labels/{version}/recovery re-issues a label for an existing shipment, which is the correct response to "the printer jammed" rather than creating a second shipment.
Void a shipment
DELETE /api/shipments/{version}/void/cancel/{shipmentidentificationnumber}, with the shipper account in a shipperNumber header and an optional trackingnumber query parameter to void one package out of a multi piece shipment rather than the whole thing. The response is VoidShipmentResponse with a Response.ResponseStatus.
Webhooks and notifications
UPS is the one carrier in this group with a real, self serve HTTP webhook, and it is the reason to prefer UPS over FedEx for a low latency tracking pipeline.
Track Alert, POST /api/track/{version}/subscription/{type}/package, subscribes a list of tracking numbers so that UPS pushes events to a URL we control.
{type}isstandard(standard event data) orenhanced(adds the detail selected byeventPreferences, currently available for package subscriptions).{version}isv2. UPS notes that v1 remains supported but recommends v2 for all new work.transIdandtransactionSrcheaders are required.- The request body takes
locale(required),countryCode,trackingNumberList(maximum 100 tracking numbers per call) and, on the enhanced type,eventPreferences, an array of 1 to 4 ofDdelivery (includes updates and delivery photo),Iin progress,Mmanifest andXexception. OmiteventPreferencesand all activity types are sent.
{
"locale": "en_US",
"countryCode": "US",
"eventPreferences": ["D", "X"],
"trackingNumberList": ["1Z1234567891234556"]
}
{
"validTrackingNumbers": ["1Z1234567891234556"],
"invalidTrackingNumbers": []
}
The response splits the batch, so a partly bad batch is a 200 with names in invalidTrackingNumbers. Always read both arrays.
The destination is configured differently between versions and this is the single most common integration mistake here. In v1 the request body carried a destination object with url, credential and credentialType (the UPS sample uses "credentialType": "Bearer"), so the receiver URL and its bearer token travelled with every subscription call. In v2 the destination object has been removed and webhook credentials are managed once, out of band, through Manage Webhook Credentials at https://developer.ups.com/update-webhook. If you copy a v1 sample into a v2 call the destination is silently ignored and no events arrive.
Verification of inbound pushes is therefore by the bearer credential UPS presents to our endpoint, the one we registered, rather than by an HMAC signature over the body. Treat that credential as the shared secret: terminate TLS, check the Authorization header on every inbound request, and reject anything else. UPS does not publish a retry policy on the reachable specifications, so design the receiver to be idempotent and back the webhook with a Quantum View sweep.
Recommended design:
- Subscribe each new 1Z number to Track Alert at ship time, batched 100 at a time.
- Take pushes as the primary stream and de-duplicate on the tuple of tracking number,
gmtDate,gmtTimeandstatus.code. - Run a nightly
POST /api/quantumview/v3/eventsover the previous 24 hours as the safety net that catches anything the webhook dropped. - Keep a slow
GET /api/track/v1/details/{inquiryNumber}reconciliation poll for shipments that have had no event for more than 48 hours, and stop polling anything past the 120 day retention window.
Rate limits and pagination
UPS does not publish numeric rate limits on any source reachable without a login, and developer.ups.com timed out for the fetcher on 2026-09-21. What is verifiable from the published specifications:
Because a 429 is documented but its threshold is not, build the client defensively: a single token per account cached until expires_in, a token bucket in front of the Rating and Shipping calls, and exponential backoff with jitter on 429 and 5xx. Ask the UPS account team for the published transactions per second and daily ceiling for the app before going live at volume.
Errors on the business APIs arrive either as Response.ResponseStatus.Code of 0 with an Alert[] and error detail, or as an HTTP error with { "response": { "errors": [ { "code": "...", "message": "..." } ] } }. Handle both. A 403 on any endpoint is documented as "Blocked Merchant", which is an account state rather than a coding error, so surface it distinctly in our onboarding UI.
Mapping to the unified model
UPS populates shipments and nothing else. The rest of the table records where the unified field has to come from our own data.
Gaps and open questions
- Numeric rate limits are not published anywhere reachable without a login, although a
429 Quota Limit Exceededis documented. Get the per app transactions per second and daily ceiling from the UPS account team before designing the sync schedule. - Legacy Developer Kit retirement date is unverified. UPS has clearly moved to OAuth and REST, and its own specification repository is REST only, but neither developer.ups.com nor ups.com would serve the deprecation notice to the fetcher on 2026-09-21. Confirm the date before assuming any legacy XML integration still works.
- Webhook retry policy and signature scheme are not published on the reachable specifications. We know the receiver is authenticated by a bearer credential registered at
developer.ups.com/update-webhookrather than by an HMAC over the body, but not how many times UPS retries or over what window. Build an idempotent receiver and keep the Quantum View sweep. - Token and refresh token lifetimes are returned per response, not fixed in the specification, so do not hard code them. Verify observed values in the Customer Integration Environment during build.
- No COD remittance and no billing API in this catalogue, so
shipping_costis the rated estimate. Decide whether to reconcile from UPS billing files. - Single element collections serialise as objects, not arrays. This is not documented as a rule in one place; it is observable across
RatedShipment,PackageResults,Package,DisclaimerandShipmentCharge. Write one normaliser and use it everywhere. - India specifics are unverified. Whether an Indian UPS account can use the same self serve Developer Portal path, and which services are rateable intra India, could not be checked from the published specifications. Confirm with the local UPS team.
Sources
- UPS Developer Portal (timed out for an unauthenticated fetcher on 2026-09-21) and ups.com/upsdeveloperkit (Access Denied on 2026-09-21)
- github.com/UPS-API/api-documentation, the official UPS OpenAPI specification files.
OAuthClientCredentials.yaml,OAuthAuthCode.yaml,Shipping.yaml,Tracking.yaml,Rating.yaml,Pickup.yaml,AddressValidation.yaml,QuantumView.yaml,TimeInTransit.yaml,UPSTrackAlert.yaml,UPSTrackAlertEnhanced.yaml,DeliveryIntercept.yaml,Locator.yaml. These supplied every endpoint path, server host, header, enum and limit quoted above - github.com/UPS-API/java-api-examples, the official UPS sample application repository.
shipping/src/main/resources/ShipmentRequest.jsonandResponse/ShipmentResponse.json,rating/src/main/resources/simpleRate.jsonandscenarios/negotiatedRateResponse.json,pickup/src/main/resources/PickupCreationRequest.json,addressValidation/src/main/resources/AddressValidationRequest.json,quantumView/src/main/resources/QuantumViewRequestByDateRange.json,pubSub/src/main/resources/pubSubRequest.json. These supplied the sample request and response bodies - github.com/UPS-API/UPS-SDKs and github.com/UPS-API/ups-mcp, the official SDK and MCP server repositories
- Magento 2 UPS carrier model, a production integration that holds both the legacy XML endpoints and the REST endpoints side by side, used here to confirm the live REST host and path values