Inferences
Shipping Label Inference
You can have the inference via the API or the vision-SDK while the vision-SDK will do the network request for you, the responses from this document will remain the same.
New Inference
To create a new inference for shipping-labels, you'll need to pass the image_url as a base64 encoded data URL or public web URL. There's a soft cap of about 2.5MB per image for the base64 URL, and the whole request body must stay under 10MB.
You can also send several images of the same package in one inference - for example a top and a bottom photo when you do not know which side carries the label. PackageX runs the models over every image, picks the one that carries the label, and returns one inference. See Multiple Images Per Inference.
You're also able to specify what type of image the parser is looking at, so it can better extract data accurately along with other helpful information listed below:
image_url string | Array.<String> (required)
A base64 encoded data URL or public web URL, or an array of up to 4 of them when you are sending several images of the same package. A web URL must be https and name a public host rather than an IP address. See Multiple Images Per Inference for the full rules.
image string | Array.<String>
An alias for image_url, accepting the same single value or array. When both are supplied, image wins. One of image or image_url is required, and null on either key counts as "not set" so the other is used - but an empty string or empty array on image does not fall back to image_url, it returns 400 inference.image. Send only the key you mean rather than blanking one out.
barcode_values Array.<String> | Array.<Array.<String>>
Any existing values you've already extracted from the barcode. These are merged with the values read from the image rather than replacing them, and the result is returned as barcode_list.
With more than one image, send an array of lists so each entry lines up with the image at the same position in image / image_url - barcode_values: [["1Z..."], []] gives the first barcode to the first image and none to the second. Each image is extracted using only its own barcodes. A single flat list sent alongside several images is taken as belonging to the first.
Because they are attached to a specific image, the values you send follow the same rule as the barcodes we read off the images: barcode_list carries the selected image's, or every image's when options.images.barcodes_from_all is set. With one image it always carries yours.
location_id String
The ID of the location you want to attribute to this scan.
layout_id String
The ID of the layout to attribute to this scan. The layout must be associated with the specified location_id.
child_shipments Array.<String>
An array of shipment IDs to attach as child shipments to the parent shipment created from this inference. This enables the parent-child shipment hierarchy where a parent shipment (e.g., a pallet or consolidated box) contains multiple child shipments. Can also be an object with add, remove, or set arrays for updating an existing inference. Only applies when the tracker type is "outbound". Child shipments must be at the same location and have a status of created, delivered, or return_to_sender.
options Object
Option contains different other optional parameters you can provide along with the image. Those parameters are described below
Show Details
match Object
All the options associated with matching contacts logic.
(These parameter are only required if you are using contact matching)
Show Details
postprocess Object
Optional postprocessing parameters
Show Details
images Object
How PackageX picks the one image that carries the shipping label when you send several, and what it keeps from the others. Ignored when you send a single image - that image is always used. Every key defaults to the matching organization setting under settings.inferences.shipping_labels.images (note that the organization object reads those settings back under the singular settings.inferences.shipping_label). An unrecognized value for classification or on_ambiguity returns 400 validation.failed; an empty string on either counts as "not set" and leaves the organization default in place. See Multiple Images Per Inference.
Show Details
tracker Object
This parameter is optional and serves to track the package. It is only required if you intend to create a tracker object. If you are using the OCR API only, you do not need to specify these parameters.
Show Details
chained_status TrackingStatusOutstanding
options?.tracker?.chained_status
auto_close_shipments_option TrackerCloseShipmentsOption
options?.tracker?.auto_close_shipments_option
This option determines how to handle outstanding shipments:
on_provider_updates: Outstanding shipments will be closed only when the provider sends a tracking update to the shipment.
on_all_updates: Outstanding shipments will be closed on any tracking update to the shipment.
off: Outstanding shipments will be ignored.
Default: on_provider_updates
auto_close_shipments_status TrackerCloseShipmentsStatus
options?.tracker?.auto_close_shipments_status
When on_all_updates or on_provider_updates is active, you can select the following completed non-exception statuses for outstanding shipments:
Multiple Images Per Inference
image and image_url each accept a single value or an array of up to 4 values, so you can send several photos of the same package in one request - for example a top and a bottom frame when you do not know which side carries the label. PackageX runs the models over every image, picks the one that carries the shipping label, and returns one inference built from that image.
Rules:
- Maximum 4 images per inference. More returns
400with the codeinference.too_many_images. - The whole request body must stay under 10MB, shared across all the images. For large photos use
image_urlrather than base64. Extraction runs at the resolution you send, so higher resolution is not wasted; theimage_urlvalues returned on the inference point at a copy downscaled to a 1024px bounding box. - An empty array returns
400inference.image. An entry that is neither a URL nor a base64 string is rejected in validation instead, as400validation.failed. - Entries may mix the two forms - one base64 data URL alongside two web URLs in the same array is fine.
- If both
imageandimage_urlcarry values,imageis used andimage_urlis ignored. Send your images through one of the two, not both. - A web URL must be 255 characters or shorter, query string included, or the request fails validation with
400validation.failed. Worth checking before you switch toimage_url: pre-signed storage links routinely exceed that. Base64 data URLs are not subject to the limit. - A web URL must be
httpsand name a host, not an address. A bare IPv4 or IPv6 literal,localhost, and the cloud metadata host are rejected with400(inference.url,inference.hostname), and a non-httpsscheme with400inference.protocol. If your images live somewhere that is not reachable over public HTTPS, send them inline as base64 data URLs instead. Data URLs are exempt from all of this. - An image we cannot fetch, decode or read fails the whole request, and nothing is persisted - no inference, no
inference.createdevent, so there is nothing to look up afterwards. That covers an unreachable or non-image URL, an undecodable base64 payload, and an OCR failure, on any one of the images. A fetch that stalls is abandoned rather than held open, and comes back as408file.timeout_error; an implausibly large file is refused as400file.too_large; and if the host serving the URL answers with a5xxyou get a502file.download, which is worth retrying, unlike the others. All of these are a different failure from the per-image extraction errors below, which are recorded on the inference and leave the other images to compete. - Multiple images are supported only when creating an inference. The update endpoint accepts no
image/image_urlat all, so anything you send there is ignored rather than rejected: an update never re-runs inference, so there is nothing for an image to act on. - Sending more than one image alongside the vision-SDK's
on_device_extractionreturns400inference.image: one already-extracted result cannot be attributed to one of several images. A single image is fine either way, whether you pass it bare or as a one-element array.
The inference itself is built from the winning image: image_url, raw_text and every extracted field come from it. media_urls lists every stored image regardless of which one won, and barcode_list can be widened to include barcodes read from the losing images too with options.images.barcodes_from_all, since a barcode on the other side of the package is still real data - by default it carries the winning image's barcodes only.
Which fields hold what, with one image versus several
Where each of these fields gets its value depends on how many images you sent:
On the missing_label and ambiguous_label paths the inference still ends in status: "error", but the extraction is kept, not discarded. Everything the images yielded is returned: raw_text, tracking_number, the carrier and party fields, the parcel dimensions, the merged barcode_list, your metadata, and an entry in images for every image carrying its own scores, document_classification and error. On ambiguous_label the top-ranked candidate is adopted into image_url and the extracted fields, because it is the answer we would have given; on missing_label no candidate was eligible, so those stay null. One field is deliberately withheld: hash is null on both paths, so re-sending the same image is not rejected as a duplicate of an inference that produced nothing. What does not happen on either path is the rest of the pipeline: no contact matching, no tracker, no media handling and no logs. errors carries that single code and nothing else, and the request fields that are normally applied after extraction are not applied either - the sender / recipient details you passed, the provider and reference-number overrides, media, a log entry, child_shipments, and the postprocess.provider_selection step that would otherwise pick between two extracted carriers (so provider reports what was read off the label, unfiltered). What you sent that does survive is metadata, the tags you set or added, notes, location_id, layout_id, weight and dimensions. The barcode_values you sent follow the same per-image rule as the barcodes we read off the images, so with several images they reach barcode_list only from the selected image - which means on missing_label, where nothing was selected, they are not carried onto the row. Set options.images.barcodes_from_all if you need them retained regardless of the outcome. With one image they are always kept.
Gate on status and errors, and on nothing else. Neither image_url nor images[].selected nor a populated tracking_number means the inference succeeded - all three can be set on a refused ambiguous_label result, where they describe the answer we would have given rather than one you should act on. This is deliberate: a refused inference that tells you what it saw is worth far more to whoever investigates it than an empty row.
Image classification modes
options.images.classification controls what role the document classifier plays over your images.
Under "classifier_feeds_extraction", an image classified as something other than a shipping label is not extracted; it is still returned in images, carrying the class in document_classification, with scores: null (there was no extraction to score) and error: null (nothing failed - it was simply never sent to the label model). If classification rules out every image, all of them are extracted anyway rather than failing the inference, and an image whose classification call itself errored is recorded as classification_failed and extracted too - a classifier outage is not evidence about the image, so it neither drops the image nor costs it any score.
How the winning image is chosen
Each image is scored on the hard signals in its extraction - a tracking number, a tracking number read from a barcode, a recognized provider (the catch-all other earns nothing, since it is no evidence of a label), a barcode being present, a complete recipient or sender address, a service level - plus the document class when a classifier ran. The two halves are reported separately as scores.extraction and scores.classification, and scores.total is their sum floored at 0. The classification half is graded by how confidently the classifier said "not a shipping label": shipping_label adds; a positive identification of a different document type is strong evidence against the image and costs the most; unknown means the classifier looked and could not place it, which is real but weak negative evidence and costs a partial penalty; and classification_failed, or no classification at all, costs nothing, because docking every image during a classifier outage would be a verdict about our infrastructure rather than about the image. The highest scores.total wins, and ties are broken by the amount of text read, then by submission order. The exact weights are internal and may be tuned; do not build logic on specific scores values, only on selected and on relative order.
The document class alone can never win an image. An image must carry evidence from its own extraction - a non-zero scores.extraction - to be considered at all; the class may reorder images that already have such evidence, but it cannot promote one that has none. Eligibility is judged on scores.extraction and not on scores.total, so an image with real extracted evidence stays in the running even when a confident non-label class floors its scores.total to 0. This follows from the classifier returning a bare class with no confidence score. A classification penalty therefore never removes an image from consideration - it only affects the ranking, and whether the image still clears the plausibility bar that feeds the ambiguity check. That is worth knowing, because it explains an otherwise puzzling outcome: a weakly-extracted image classified unknown can still be the one that gets selected, while no longer counting as a competing label for the purpose of declaring the batch ambiguous. So two blank package sides that the classifier happens to call shipping_label give missing_label, not an ambiguous pair.
If no image is eligible, what you get back depends on whose fault it was. A per-image 4xx is a real answer about that image - 418 image.no_text for a blank side, 400 image.invalid - so the inference ends in missing_label with status: "error" and a successful HTTP status. But if any of the images failed with a 5xx instead, or with no recognizable status at all, that upstream error becomes the request's error: the call fails with the upstream status rather than returning a 200 carrying missing_label. The inference row is still recorded, with status: "error" and each image's own error, so you can see what happened. Laundering an outage into missing_label would have you divert every parcel while the real fault went unnoticed.
A single image is always used. Scoring exists only to choose between images, so it can never reject the only image you sent. missing_label and ambiguous_label are therefore only possible when you send 2 or more images.
Ambiguity
Two frames of the same package showing the same label is the normal case and is not ambiguous - the higher-scoring one simply wins. An inference is ambiguous when two or more images both look like shipping labels and they cannot be reconciled:
- their tracking numbers differ (so this may be two packages, or a misread), or
- none of them produced a tracking number (so there is nothing to tell them apart).
options.images.on_ambiguity decides what happens:
"use_best"(default) uses the highest-scoring image and proceeds. Nothing in the response says the batch was ambiguous: no field records it, and because only the winner's extraction is returned you cannot compare the losing tracking numbers yourself. The other candidates are listed inimageswith their scores, so a close second is a hint that something was there to choose between - but it is only a hint. If your process needs to know for certain, use"error"."error"refuses to guess:statusis"error"anderrorscontainsambiguous_label. The extraction is still returned in full - the top-ranked candidate's fields are on the inference and every image'sscoresare inimages- but the pipeline stops there: no matching, no tracker. Treat the populated fields as a report, not a result, and gate onerrors.
Use "error" when acting on the wrong package is worse than handling an exception - an automated conveyor, for example.
New response fields
images Array.<Object> | null
One entry per image you sent, in the order you sent them - present only when you sent two or more. null when you sent a single image, since there was nothing to choose between and the inference's own fields already describe it, and null on inferences created before multiple images were supported.
Show Details
| images[].selected Boolean
true on the one image the inference was built from. false on every image when the inference ends in missing_label; on ambiguous_label the top-ranked candidate carries true and its extraction is on the inference, even though the result was refused - so pair it with errors before treating it as the label. image_url is not a substitute: it is populated on that path too. |
| images[].error Object | null
{ code, message, status } when this image's extraction failed - code and message are each String | null and status a Number | null, since an upstream failure does not always carry all three - and set only on a failure: an image that classification ruled out before extraction has error: null, because nothing failed, it was simply never sent to the label model. A per-image error is not the request's error - with several images, a blank package side failing to read is expected and is simply strong evidence that it is not the label. With a single image there is no fallback, so a failed extraction fails the request instead. |
A two-image images array, under the default "from_extraction_only", where the second frame was a blank package side:
Those seven keys are the whole entry. The score numbers are illustrative - they are relative, and the weights may be tuned.
Only the winning image's extraction is returned: what the winner extracted is what you read off the inference itself (raw_text, provider, tracking_number, and the rest), and the other images' extractions are not part of the response - a losing image is described by its scores, document_classification and error alone. The raw OCR payloads, image hashes and image metadata are recorded internally and are not returned either.
New error codes
Both appear in the inference's errors array with status: "error", and both are returned with a successful HTTP status - the failure is in the body so a single response tells you everything. When either is set it is the only entry in errors, since the inference stops before the steps that raise the others. Neither can occur with a single image.
Two request-level codes belong to the same feature, and both are real HTTP 400s rather than body errors: inference.too_many_images for more than 4 images, and inference.image for an image that is missing, blank, or sent as more than one image alongside the vision-SDK's on_device_extraction. Malformed option values (a misspelled classification, a non-boolean barcodes_from_all, a web URL over 255 characters) fail validation as 400 validation.failed.
missing_label also exists as an inference exception, where it means an operator flagged the physical package as having no label. The error in errors and the flag in exceptions are different fields set by different paths - check the field, not just the string.
Synchronous Tracker Creation
By default, when options.tracker.create_automatically is true the tracker is created shortly after the inference response is returned: the create response comes back with status: "transforming" and tracker still null, and you learn about the tracker from the inference.transformed webhook or by polling.
Setting options.tracker.create_synchronously to true creates the tracker inside the request, so the response already carries status: "completed" and tracker.id. Nothing to poll for. This is intended for callers on a hard deadline - an automated conveyor that must know, before the package reaches the next checkpoint, whether it was accepted or must be diverted.
Things to know before enabling it:
- It only works alongside
create_automatically: true. On its own it does nothing; it is forced tofalsewhencreate_automaticallyis not set. - A key you leave out falls back to your organization setting, including
create_automatically. Sendingoptions: { tracker: { create_synchronously: true } }on its own leaves auto-creation tosettings.inferences.shipping_labels.tracker.create_automatically, so it creates a tracker synchronously if that setting is on, and creates nothing if it is off. Sendcreate_automatically: trueexplicitly when you want it regardless of the organization setting. The example above does that, and names atype, so it reads the same whatever the organization is configured to do. - It applies to later writes on the same inference too, not only to the create. The flag is stored on the inference, so if a follow-up update is what finally moves it to
transforming- you supplied the contact that was missing, for example - that update creates the tracker inside its own request and returns it, the same way the create would have. - It applies only when the inference reached
transforming. An inference that ends instatus: "error"- no confident contact match, an ambiguous label, a missing required property - does not get a tracker, synchronously or otherwise. - Failures are reported in the body, not as an HTTP error. If tracker creation fails, the response still succeeds and the inference comes back with
status: "error"and the reason inerrors. This is deliberate, so an automated caller can branch on one response instead of handling a body in one path and an HTTP error in another. Never treat a successful HTTP status alone as "the package exists" - checkstatusandtracker.id. - Webhook order and count are unchanged. You get the same two events in the same order either way:
inference.createdcarryingstatus: "transforming", theninference.transformed. The only difference is when they arrive - both are emitted during the request, soinference.transformedcan reach your endpoint while you are still reading the HTTP response. Note thatinference.transformedis emitted on failure as well as success, so readstatuson the payload rather than treating the event itself as confirmation that a tracker exists. See Events. "outbound"trackers are slower. An outbound tracker registers the package with the carrier inside your request, adding a third-party network call to your critical path. An"inbound"tracker does not. Prefer"inbound"for latency-sensitive integrations, and budget for the extra hop if you need"outbound".
Inbound Rerouting (Multi-Hop)
When inbound multi-hop rerouting is enabled at the organization or location level, the system can automatically detect when a package does not have a recipient match at the scanned location and attempt to find the correct recipient across all locations in the organization.
If a match is found at a different location, the inference response will include a destination_location object indicating where the package should be rerouted to.
If the system matches a recipient but cannot determine the destination location, the inference will include a destination_location_unknown error. You can resolve this by updating the inference with a destination_location_id:
Rerouting only applies to inbound scans and recipient matching. It will not trigger for outbound scans or sender matching. The feature must be enabled in your organization settings (settings.inferences.shipping_labels.inbound_multi_hop_enabled) and can be overridden per location.
Inference Exceptions
The Exceptions feature allows us to flag specific shipping label inferences that contain anomalies or irregularities detected during processing. When an inference is in exception state, it can not be transformed however it can be reviewd and resolved out of the exception state and then it can be transformed.
This capability is useful in scenarios where the OCR model successfully processes the label image but encounters issues that affect the quality or reliability of the extracted data. Examples include:
unknown_contact - Contact information cannot be reliably identified.
addressed_generically - The label is addressed to a generic title (e.g., "Customer" or "Resident").
damaged_label - The physical label appears partially destroyed or unreadable.
suspicious - The label contains patterns that may indicate fraud or tampering.
arrived_opened - The package is detected to have been opened prior to delivery.
We can update an existing inference record and set it to exception, along with appropriate exceptions type.