> ## Documentation Index
> Fetch the complete documentation index at: https://docs.smartsend.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Developer reference

> Hooks, metadata and PHP APIs for customizing Smart Send and reading WooCommerce order delivery and booking data.

Customize Smart Send with WordPress filters and actions in your own plugin. Hooks let you adjust checkout, delivery choices and booking behavior without editing Smart Send. Use the [metadata reference](#metadata) to understand stored values and the [PHP API](#api) to read pickup points, shipping methods and bookings for a WooCommerce order.

## Filters and actions

<span id="choose-the-right-stage" />

Choose the stage that matches your customization. Each stage and hook below links to its detailed contract and example.

| Stage                                                               | Purpose                                                             | Useful hooks                                                                                                                                                                                                                                               |
| ------------------------------------------------------------------- | ------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Checkout availability and rates](#checkout-availability-and-rates) | Control which rates are offered and their prices.                   | [`woocommerce_shipping_smart_send_shipping_is_available`](#woocommerce-shipping-smart-send-shipping-is-available), [`woocommerce_smart_send_shipping_shipping_add_rate`](#woocommerce-smart-send-shipping-shipping-add-rate)                               |
| [Pickup points](#pickup-points)                                     | Adjust pickup lookup, result ordering and labels.                   | [`smart_send_pickup_points_found`](#smart-send-pickup-points-found), [`smart_send_pickup_point_label`](#smart-send-pickup-point-label)                                                                                                                     |
| [Shipping method settings](#shipping-method-settings)               | Add fields to global or per-method settings.                        | [`woocommerce_settings_api_form_fields_smart_send_shipping`](#woocommerce-settings-api-form-fields-smart-send-shipping), [`woocommerce_shipping_instance_form_fields_smart_send_shipping`](#woocommerce-shipping-instance-form-fields-smart-send-shipping) |
| [Delivery decisions](#fulfillment-decisions)                        | Choose the carrier service and parcel plan for a booking direction. | [`smart_send_delivery_details`](#smart-send-delivery-details), [`smart_send_parcel_default_weight`](#smart-send-parcel-default-weight)                                                                                                                     |
| [Order data](#order-data-for-booking)                               | Prepare recipient, item and customs data.                           | [`smart_send_order_receiver`](#smart-send-order-receiver), [`smart_send_payload_items`](#smart-send-payload-items)                                                                                                                                         |
| [Booking](#booking-request-and-events)                              | Adjust the request or react to a carrier booking result.            | [`smart_send_booking_request`](#smart-send-booking-request), [`smart_send_booking_completed`](#smart-send-booking-completed)                                                                                                                               |
| [Order updates](#order-side-effects-and-final-result)               | Control notes, tracking and status after booking.                   | [`smart_send_fulfillment_tracking`](#smart-send-fulfillment-tracking), [`smart_send_order_fulfilled`](#smart-send-order-fulfilled)                                                                                                                         |
| [Environment and logging](#environment-logging-and-plugin-links)    | Configure the API endpoint, logs and plugin links.                  | [`smart_send_api_endpoint`](#smart-send-api-endpoint), [`smart_send_logging`](#smart-send-logging)                                                                                                                                                         |

`$is_return` is `false` for outbound and `true` for return wherever the signature includes it. A combined booking can run a callback twice, once for each direction. Events with a `Booked_Shipment` instead expose `$shipment->is_return()`.

<Note>
  `smart_send_booking_completed` means Smart Send has created one shipment. It fires before the plugin saves its booking information to the WooCommerce order. Use `smart_send_order_fulfilled` when you need the final order-side result, including an outbound success with a return failure.
</Note>

Signatures list the filter value first, followed by context arguments; `→` states the required return type. Actions have no useful return value. Examples are independent customizations, not a file to install in full.

Open a hook and select **Description** for its contract or **Example** for the PHP customization and usage guidance.

### Checkout availability and rates

<AccordionGroup>
  <Accordion title="woocommerce_shipping_smart_send_shipping_is_available" id="woocommerce-shipping-smart-send-shipping-is-available">
    <Tabs>
      <Tab title="Description">
        **Filter** · `(bool $is_available, array $package, Smart_Send\Shipping_Method\Method $method) → bool`

        Runs after the method's shipping-class and customer-role checks. Return `false` to hide this Smart Send instance for the package; return the incoming value to preserve its existing restrictions. Returning `true` does not create a price outside the weight table. Checkout only; no booking or order updates. This example additionally limits the method to Denmark.
      </Tab>

      <Tab title="Example">
        ```php theme={null}
        add_filter( 'woocommerce_shipping_smart_send_shipping_is_available', function ( bool $available, array $package, \Smart_Send\Shipping_Method\Method $method ): bool {
        	return $available && 'DK' === ( $package['destination']['country'] ?? '' );
        }, 10, 3 );
        ```
      </Tab>
    </Tabs>
  </Accordion>

  <Accordion title="woocommerce_shipping_smart_send_shipping_is_free_shipping" id="woocommerce-shipping-smart-send-shipping-is-free-shipping">
    <Tabs>
      <Tab title="Description">
        **Filter** · `(bool $is_free, array $package, Smart_Send\Shipping_Method\Method $method) → bool`

        Runs after the configured minimum/coupon condition is evaluated, when the cart weight is allowed. `true` selects **Flat fee cost**, which is not necessarily zero; `false` uses normal weight-price calculation. It does not override zone, role, class or weight availability. The example disables the flat-fee branch, preserving ordinary rate calculation.
      </Tab>

      <Tab title="Example">
        ```php theme={null}
        add_filter( 'woocommerce_shipping_smart_send_shipping_is_free_shipping', '__return_false' );
        ```
      </Tab>
    </Tabs>
  </Accordion>

  <Accordion title="woocommerce_smart_send_shipping_shipping_add_rate" id="woocommerce-smart-send-shipping-shipping-add-rate">
    <Tabs>
      <Tab title="Description">
        **Action** · `(Smart_Send\Shipping_Method\Method $method, array $rate) → void`

        Runs at the end of a Smart Send rate calculation. `$rate` contains `id`, `label`, `cost`, `meta_data` and `package`. It can run even when weight rules produced no rate, so check `$method->rates` before deriving an extra option. The example adds 10 in the store currency to an existing rate, before WooCommerce tax calculation. It preserves the original service metadata; it does not book a different carrier service.
      </Tab>

      <Tab title="Example">
        ```php theme={null}
        add_action( 'woocommerce_smart_send_shipping_shipping_add_rate', function ( \Smart_Send\Shipping_Method\Method $method, array $rate ): void {
        	if ( ! isset( $method->rates[ $rate['id'] ] ) ) {
        		return;
        	}
        	$rate['cost']  = (float) $method->rates[ $rate['id'] ]->get_cost() + 10;
        	$rate['id']   .= ':extra-handling';
        	$rate['label'] = 'Delivery with extra handling';
        	$method->add_rate( $rate );
        }, 10, 2 );
        ```
      </Tab>
    </Tabs>
  </Accordion>
</AccordionGroup>

### Pickup points

<AccordionGroup>
  <Accordion title="smart_send_pickup_point_search_params" id="smart-send-pickup-point-search-params">
    <Tabs>
      <Tab title="Description">
        **Filter** · `(array $params) → array`

        Runs before a nearest-point API lookup when a token is configured. Keys: `carrier`, `country`, `postal_code`, `street` (strings) and `city` (string or null). Return all keys. The API determines result count; trim results with `smart_send_pickup_points_found`. Search parameters do not rewrite the customer's order address. This example removes surrounding postal-code whitespace.
      </Tab>

      <Tab title="Example">
        ```php theme={null}
        add_filter( 'smart_send_pickup_point_search_params', function ( array $params ): array {
        	$params['postal_code'] = trim( $params['postal_code'] );
        	return $params;
        } );
        ```
      </Tab>
    </Tabs>
  </Accordion>

  <Accordion title="smart_send_pickup_points_found" id="smart-send-pickup-points-found">
    <Tabs>
      <Tab title="Description">
        **Filter** · `(Smart_Send\Delivery\Pickup_Point[] $points, array $params) → Smart_Send\Delivery\Pickup_Point[]`

        Runs after a non-empty API result has become typed points, before session caching/display. Return a reindexed list of compatible `Pickup_Point` objects. The plugin removes objects that do not match the lookup's carrier/country. It does not call this filter for an empty API result. Reordering influences **Select Default** only when no compatible explicit customer choice exists; this is not a way to revoke an existing compatible selection.
      </Tab>

      <Tab title="Example">
        This example puts points with a Saturday opening interval first, preserving the existing distance order within each group, then offers at most five. It does not invent or modify pickup addresses. An unknown opening-hours list is treated as not known to be open Saturday.

        ```php theme={null}
        add_filter( 'smart_send_pickup_points_found', function ( array $points, array $params ): array {
        	$saturday = array();
        	$other    = array();
        	foreach ( $points as $point ) {
        		$open_saturday = false;
        		foreach ( $point->get_opening_hours() as $interval ) {
        			if ( 'saturday' === $interval['day'] ) {
        				$open_saturday = true;
        				break;
        			}
        		}
        		if ( $open_saturday ) {
        			$saturday[] = $point;
        		} else {
        			$other[] = $point;
        		}
        	}
        	return array_slice( array_merge( $saturday, $other ), 0, 5 );
        }, 10, 2 );
        ```

        **Expected result:** for nearest-first points A (Monday), B (Saturday), C (Saturday), D (unknown), the order is B, C, A, D. With **Select Default** enabled and no explicit selection, B becomes the default. A compatible point explicitly chosen by the customer remains selected even if it is outside the new top five. The filter is for offering/reordering candidates, not invalidating that choice. Test with a refreshed address because results can be cached in the checkout session.
      </Tab>
    </Tabs>
  </Accordion>

  <Accordion title="smart_send_pickup_point_label" id="smart-send-pickup-point-label">
    <Tabs>
      <Tab title="Description">
        **Filter** · `(string $label, Smart_Send\Delivery\Pickup_Point $point) → string`

        Runs when a pickup label is formatted, after **Dropdown format**. Return plain text, not HTML or pre-escaped entities. Each renderer escapes its output. This affects display, not the point's identity or saved address. Identity is the exact agent number together with carrier and country.
      </Tab>

      <Tab title="Example">
        Append the agent number to the existing label in Classic Checkout and Checkout Block. Return unescaped plain text: an ampersand in a company name must remain `&`, not `&amp;`. The renderer handles escaping. The saved point does not change.

        ```php theme={null}
        add_filter( 'smart_send_pickup_point_label', function ( string $label, \Smart_Send\Delivery\Pickup_Point $point ): string {
        	return $label . ' (#' . $point->get_agent_no() . ')';
        }, 10, 2 );
        ```

        **Expected result:** `Corner Shop & Kiosk (#00123)`. Leading zeroes in the agent number are preserved. Verify both checkout types and confirm the selected point on the order.
      </Tab>
    </Tabs>
  </Accordion>

  <Accordion title="smart_send_pickup_point_timeout" id="smart-send-pickup-point-timeout">
    <Tabs>
      <Tab title="Description">
        **Filter** · `(int $seconds) → int|float`

        Default: 4 seconds. Runs for both nearest-point and single-point API requests. Return a positive timeout in seconds. Increasing it can lengthen checkout waits; it does not guarantee results. The example allows six seconds.
      </Tab>

      <Tab title="Example">
        ```php theme={null}
        add_filter( 'smart_send_pickup_point_timeout', function ( int $seconds ): int {
        	return 6;
        } );
        ```
      </Tab>
    </Tabs>
  </Accordion>
</AccordionGroup>

### Shipping method settings

<AccordionGroup>
  <Accordion title="woocommerce_settings_api_form_fields_smart_send_shipping" id="woocommerce-settings-api-form-fields-smart-send-shipping">
    <Tabs>
      <Tab title="Description">
        **Filter** · `(array $fields) → array`

        WooCommerce calls this when reading the global Smart Send settings field definitions. Return the complete field array. Editing a field's description/default does not change a previously saved value or implement behavior for a new field. The example adds guidance to the existing validation button.
      </Tab>

      <Tab title="Example">
        ```php theme={null}
        add_filter( 'woocommerce_settings_api_form_fields_smart_send_shipping', function ( array $fields ): array {
        	$fields['api_token_validate']['description'] = 'Save the API token before validating it.';
        	return $fields;
        } );
        ```
      </Tab>
    </Tabs>
  </Accordion>

  <Accordion title="woocommerce_shipping_instance_form_fields_smart_send_shipping" id="woocommerce-shipping-instance-form-fields-smart-send-shipping">
    <Tabs>
      <Tab title="Description">
        **Filter** · `(array $fields) → array`

        WooCommerce calls this when reading a shipping-zone instance's fields. Return the complete definitions. The example clarifies the customer-facing title; it changes neither the booked service nor saved settings.
      </Tab>

      <Tab title="Example">
        ```php theme={null}
        add_filter( 'woocommerce_shipping_instance_form_fields_smart_send_shipping', function ( array $fields ): array {
        	$fields['title']['description'] = 'Shown to the customer at checkout.';
        	return $fields;
        } );
        ```
      </Tab>
    </Tabs>
  </Accordion>
</AccordionGroup>

### Fulfillment decisions

<AccordionGroup>
  <Accordion title="smart_send_fulfillment_shipping_methods" id="smart-send-fulfillment-shipping-methods">
    <Tabs>
      <Tab title="Description">
        **Filter** · `(array $carriers, WC_Order $order, bool $is_return) → array`

        Runs separately for the outbound and return service lists on the order screen. Shape: each carrier has `code`, `name`, `services`; each service has `code`, `name`, `addons` (currently an empty array). A booking method is `carrier_service`, for example `postnord_agent`. Return the filtered carrier list. The resolved selected method is always re-added if missing, even if you return `[]`. This narrows the UI; it is not an authorization boundary. Enforce booking rules in `smart_send_delivery_details`. The example offers only PostNord for outbound, subject to that selected-method safeguard.
      </Tab>

      <Tab title="Example">
        ```php theme={null}
        add_filter( 'smart_send_fulfillment_shipping_methods', function ( array $carriers, \WC_Order $order, bool $is_return ): array {
        	if ( $is_return ) {
        		return $carriers;
        	}
        	return array_values( array_filter( $carriers, function ( array $carrier ): bool {
        		return 'postnord' === $carrier['code'];
        	} ) );
        }, 10, 3 );
        ```
      </Tab>
    </Tabs>
  </Accordion>

  <Accordion title="smart_send_delivery_details" id="smart-send-delivery-details">
    <Tabs>
      <Tab title="Description">
        **Filter** · `(Smart_Send\Delivery\Delivery_Details $details, WC_Order $order, bool $is_return) → Smart_Send\Delivery\Delivery_Details`

        Runs once per attempted direction after stored configuration, resolved service and submitted overrides are merged, before pickup validation and booking. Return the details object. Use `set_shipping_method()`, `set_pickup_point()` or `set_parcel_plan()`; use `clear_pickup_point()` for an explicit clear in a partial details object. Throw `Smart_Send\Booking\Exceptions\Booking_Exception` to reject a business rule before booking. Filter changes affect this booking; they are not automatically persisted. See [delivery details and persistence](#delivery-details-and-persistence).
      </Tab>

      <Tab title="Example">
        This narrowly scoped example splits an outbound order with exactly one line and three whole units into two colli: 2 + 1. It leaves return bookings and any existing or submitted parcel plan unchanged. Adapt the matching rule to the products and packing process you actually use before enabling it.

        The IDs passed to `add_item()` are **WooCommerce order-item IDs**, not product or variation IDs. Measurements below are in centimeters. Weight remains automatic; the [packaging-weight example](#smart-send-parcel-default-weight) can therefore apply to each colli.

        ```php theme={null}
        add_filter( 'smart_send_delivery_details', function ( \Smart_Send\Delivery\Delivery_Details $details, \WC_Order $order, bool $is_return ): \Smart_Send\Delivery\Delivery_Details {
        	if ( $is_return || null !== $details->get_parcel_plan() ) {
        		return $details;
        	}
        	$items = array_values( $order->get_items() );
        	if ( 1 !== count( $items ) || 3.0 !== (float) $items[0]->get_quantity() ) {
        		return $details;
        	}
        	$item  = $items[0];
        	$first = new \Smart_Send\Delivery\Parcel_Spec();
        	$first->add_item( $item->get_id(), 2, $item->get_name() )
        		->set_length( 30 )->set_width( 20 )->set_height( 15 );
        	$second = new \Smart_Send\Delivery\Parcel_Spec();
        	$second->add_item( $item->get_id(), 1, $item->get_name() )
        		->set_length( 30 )->set_width( 20 )->set_height( 10 );
        	$plan = new \Smart_Send\Delivery\Parcel_Plan();
        	$plan->add_spec( $first )->add_spec( $second );
        	return $details->set_parcel_plan( $plan );
        }, 10, 3 );
        ```

        **Expected result:** an order line with ID 701 and quantity 3 becomes allocations `(701, 2)` and `(701, 1)`. All three units are allocated once. For a 0.50 kg product and 0.20 kg packaging per colli, the weights are 1.20 kg and 0.70 kg. Missing product weight still needs correction or an explicit parcel weight. The generated plan controls this booking; filter output is not automatically saved as the order's parcel configuration. See [Colli shipments](/integrations/woocommerce/shipping-labels#colli-shipments) for the merchant workflow.
      </Tab>
    </Tabs>
  </Accordion>

  <Accordion title="smart_send_parcel_default_weight" id="smart-send-parcel-default-weight">
    <Tabs>
      <Tab title="Description">
        **Filter** · `(float $weight, Smart_Send\Delivery\Parcel_Spec $spec, WC_Order $order) → float`

        Runs per parcel without an explicit weight. Input is the allocated items' total in **kg**, possibly zero. Return a finite usable weight including packaging. An explicit `Parcel_Spec::set_weight()` or order-screen weight bypasses the filter. An allocated deleted product with unknown weight produces a validation error before the filter; provide an explicit positive parcel weight instead. There is no direction argument; the filter can run for outbound and return.
      </Tab>

      <Tab title="Example">
        Add 0.20 kg of packaging to each parcel whose weight is computed from its items. The filter uses kilograms regardless of the shop's catalog weight unit. It can run for both outbound and return parcels.

        ```php theme={null}
        add_filter( 'smart_send_parcel_default_weight', function ( float $weight, \Smart_Send\Delivery\Parcel_Spec $spec, \WC_Order $order ): float {
        	return $weight + 0.20;
        }, 10, 3 );
        ```

        **Expected result:** item weights totaling 1.50 kg become 1.70 kg. An explicitly entered parcel weight of 2.00 kg stays 2.00 kg; it already represents the total including packaging. A deleted product with unknown weight still requires an explicit positive parcel weight. This filter does not change checkout weight-band prices or saved product weights.
      </Tab>
    </Tabs>
  </Accordion>
</AccordionGroup>

### Order data for booking

<AccordionGroup>
  <Accordion title="smart_send_order_receiver" id="smart-send-order-receiver">
    <Tabs>
      <Tab title="Description">
        **Filter** · `(array $shipping_address, int $order_id) → array`

        Runs when reading the order's shipping address, before phone/email fallback and conversion to receiver data. Uses WooCommerce address keys such as `first_name`, `last_name`, `company`, `address_1`, `address_2`, `postcode`, `city`, `country`, and optional `phone`/`email`. Return the complete address. The second argument is the order ID, not a `WC_Order`. Both directions use the same reading stage; this filter does not save the address.
      </Tab>

      <Tab title="Example">
        ```php theme={null}
        add_filter( 'smart_send_order_receiver', function ( array $address, int $order_id ): array {
        	$address['country'] = strtoupper( $address['country'] );
        	return $address;
        }, 10, 2 );
        ```
      </Tab>
    </Tabs>
  </Accordion>

  <Accordion title="smart_send_receiver_phone" id="smart-send-receiver-phone">
    <Tabs>
      <Tab title="Description">
        **Filter** · `(string|null $phone, WC_Order $order) → string|null`

        Runs after choosing shipping phone, or billing phone when shipping and billing countries match, then trimming whitespace. Return a phone number or `null`. No direction flag is supplied. The example removes spaces without guessing a country calling code.
      </Tab>

      <Tab title="Example">
        ```php theme={null}
        add_filter( 'smart_send_receiver_phone', function ( ?string $phone, \WC_Order $order ): ?string {
        	return null === $phone ? null : preg_replace( '/\s+/', '', $phone );
        }, 10, 2 );
        ```
      </Tab>
    </Tabs>
  </Accordion>

  <Accordion title="smart_send_payload_receiver" id="smart-send-payload-receiver">
    <Tabs>
      <Tab title="Description">
        **Filter** · `(array $receiver, WC_Order $order) → array`

        Runs after receiver mapping and phone/email fallback. Keys: `company`, `name_line1`, `name_line2`, `address_line1`, `address_line2`, `postal_code`, `city`, `country` (strings), `phone` and `email` (string or null). Return all keys. This is plugin-level data, not a raw HTTP payload, and it is used in either direction. The example removes accidental whitespace around a non-null email.
      </Tab>

      <Tab title="Example">
        ```php theme={null}
        add_filter( 'smart_send_payload_receiver', function ( array $receiver, \WC_Order $order ): array {
        	if ( null !== $receiver['email'] ) {
        		$receiver['email'] = trim( $receiver['email'] );
        	}
        	return $receiver;
        }, 10, 2 );
        ```
      </Tab>
    </Tabs>
  </Accordion>

  <Accordion title="smart_send_payload_items" id="smart-send-payload-items">
    <Tabs>
      <Tab title="Description">
        **Filter** · `(array[] $items, WC_Order $order) → array[]`

        Runs after reading one row per order line, before allocation to parcels, in either direction. Return the same identities and valid quantities; use `smart_send_delivery_details` to distribute them. See the [item row schema](#item-rows). Net/tax values are discounted **line totals**, not unit prices. The example trims a customs description without fabricating one for a deleted product.
      </Tab>

      <Tab title="Example">
        ```php theme={null}
        add_filter( 'smart_send_payload_items', function ( array $items, \WC_Order $order ): array {
        	foreach ( $items as &$item ) {
        		if ( is_string( $item['description'] ) ) {
        			$item['description'] = trim( $item['description'] );
        		}
        	}
        	unset( $item );
        	return $items;
        }, 10, 2 );
        ```
      </Tab>
    </Tabs>
  </Accordion>

  <Accordion title="smart_send_payload_totals" id="smart-send-payload-totals">
    <Tabs>
      <Tab title="Description">
        **Filter** · `(array $totals, WC_Order $order) → array`

        Runs after order totals have been derived and negative net/tax values clamped to zero. Return all keys in the [totals schema](#totals). Later values are used as returned; preserve reconciliation and non-negative amounts. Order-level fees are included in shipment totals, whereas parcel totals contain allocated merchandise. The example logs only the currency and order total, leaving the booking values unchanged.
      </Tab>

      <Tab title="Example">
        ```php theme={null}
        add_filter( 'smart_send_payload_totals', function ( array $totals, \WC_Order $order ): array {
        	wc_get_logger()->info( 'Booking value', array(
        		'source'   => 'my-shop-shipping',
        		'currency' => $totals['currency'],
        		'total'    => $totals['total_net_amount'] + $totals['total_tax_amount'],
        	) );
        	return $totals;
        }, 10, 2 );
        ```
      </Tab>
    </Tabs>
  </Accordion>

  <Accordion title="smart_send_shipment_freetext" id="smart-send-shipment-freetext">
    <Tabs>
      <Tab title="Description">
        **Filter** · `(string|null $freetext, WC_Order $order) → string|null`

        Runs when reading text for the label, in either direction. Input is the customer's order comment when **Include order comment on label** is enabled, otherwise `null`. Return text or `null`; do not return HTML. This does not control WooCommerce order-history notes. The example omits customer comments from labels.
      </Tab>

      <Tab title="Example">
        ```php theme={null}
        add_filter( 'smart_send_shipment_freetext', '__return_null' );
        ```
      </Tab>
    </Tabs>
  </Accordion>
</AccordionGroup>

### Booking request and events

<AccordionGroup>
  <Accordion title="smart_send_booking_request" id="smart-send-booking-request">
    <Tabs>
      <Tab title="Description">
        **Filter** · `(Smart_Send\Booking\Shipment $shipment, WC_Order $order, bool $is_return) → Smart_Send\Booking\Shipment`

        Runs after data extraction and parcel construction, immediately before translation into the API request. Return a `Shipment`. Prefer earlier filters for allocation/weight rules because this is after parcel validation. The example adds an outbound-only internal reference; it does not update the order or persist delivery settings.
      </Tab>

      <Tab title="Example">
        ```php theme={null}
        add_filter( 'smart_send_booking_request', function ( \Smart_Send\Booking\Shipment $shipment, \WC_Order $order, bool $is_return ): \Smart_Send\Booking\Shipment {
        	if ( ! $is_return ) {
        		$shipment->set_internal_reference( 'WEB-' . $order->get_order_number() );
        	}
        	return $shipment;
        }, 10, 3 );
        ```
      </Tab>
    </Tabs>
  </Accordion>

  <Accordion title="smart_send_booking_completed" id="smart-send-booking-completed">
    <Tabs>
      <Tab title="Description">
        **Action** · `(Smart_Send\Booking\Booked_Shipment $booked, Smart_Send\Booking\Shipment $request, WC_Order $order) → void`

        Runs once per successfully booked direction, after the API response is mapped and before order-side persistence or document copying. Use `$booked->is_return()` for direction. It does not mean the whole outbound/return run succeeded, and a document's local copy is not ready yet. Do not throw from the listener: a shipment already exists. Use `smart_send_order_fulfilled` when you need stored order results.
      </Tab>

      <Tab title="Example">
        Record the shipment identity and direction without relying on order metadata that has not yet been saved. This callback runs separately for outbound and return, including when outbound succeeds and return later fails.

        ```php theme={null}
        add_action( 'smart_send_booking_completed', function ( \Smart_Send\Booking\Booked_Shipment $booked, \Smart_Send\Booking\Shipment $request, \WC_Order $order ): void {
        	wc_get_logger()->info( 'Smart Send shipment booked', array(
        		'source'      => 'my-shop-shipping',
        		'order_id'    => $order->get_id(),
        		'shipment_id' => $booked->get_shipment_id(),
        		'direction'   => $booked->is_return() ? 'return' : 'outbound',
        	) );
        }, 10, 3 );
        ```

        **Expected result:** one entry per created shipment in WooCommerce logs under `my-shop-shipping`. No entry is added for a rejected booking. This example does not create another shipment, save an order note or send customer notifications. For an external integration, use the shipment ID as its idempotency key and handle delivery failures without throwing from this callback.
      </Tab>
    </Tabs>
  </Accordion>

  <Accordion title="smart_send_booking_failed" id="smart-send-booking-failed">
    <Tabs>
      <Tab title="Description">
        **Action** · `(Smart_Send\Booking\Exceptions\Booking_Exception $exception, Smart_Send\Booking\Shipment $request, WC_Order $order) → void`

        Runs when the booking API call throws a handled HTTP client exception, just before it is rethrown as `Booking_Exception`. Read `getMessage()`, `errors()` (field → message list), and `response_id()` (string or null). This is not an all-failures event: local allocation, pickup or delivery validation can fail before the API call. No direction boolean is provided. The example logs a response ID, without address data or field values.
      </Tab>

      <Tab title="Example">
        ```php theme={null}
        add_action( 'smart_send_booking_failed', function ( \Smart_Send\Booking\Exceptions\Booking_Exception $error, \Smart_Send\Booking\Shipment $request, \WC_Order $order ): void {
        	wc_get_logger()->warning( 'Smart Send booking failed', array(
        		'source'      => 'my-shop-shipping',
        		'order_id'    => $order->get_id(),
        		'response_id' => $error->response_id(),
        	) );
        }, 10, 3 );
        ```
      </Tab>
    </Tabs>
  </Accordion>
</AccordionGroup>

### Order side effects and final result

<AccordionGroup>
  <Accordion title="smart_send_fulfillment_save_documents" id="smart-send-fulfillment-save-documents">
    <Tabs>
      <Tab title="Description">
        **Filter** · `(bool $save, Smart_Send\Booking\Booked_Shipment $shipment, WC_Order $order) → bool`

        Runs after successful booking and persistence of submitted delivery overrides, before document-copy attempts and shipment-history persistence. Defaults to **Save a copy of the PDF** for either direction. Return `false` to skip local copies. A copy failure adds a warning; the booked shipment remains fulfilled and document URLs fall back to Smart Send. The example keeps documents remote.
      </Tab>

      <Tab title="Example">
        ```php theme={null}
        add_filter( 'smart_send_fulfillment_save_documents', '__return_false' );
        ```
      </Tab>
    </Tabs>
  </Accordion>

  <Accordion title="smart_send_fulfillment_order_note" id="smart-send-fulfillment-order-note">
    <Tabs>
      <Tab title="Description">
        **Filter** · `(string $note_html, Smart_Send\Booking\Booked_Shipment $shipment, WC_Order $order) → string`

        Runs after shipment history is saved, before the native order note is added. Return safe HTML/text, or `''` to omit the note. Notes are saved using `WC_Order::add_order_note()` when the caller requests a note. WooCommerce's history refreshes after a page reload; the Smart Send result can appear immediately. The example writes a short plain-text note for either direction.
      </Tab>

      <Tab title="Example">
        ```php theme={null}
        add_filter( 'smart_send_fulfillment_order_note', function ( string $note, \Smart_Send\Booking\Booked_Shipment $shipment, \WC_Order $order ): string {
        	return esc_html( 'Smart Send shipment ' . $shipment->get_shipment_id() );
        }, 10, 3 );
        ```
      </Tab>
    </Tabs>
  </Accordion>

  <Accordion title="smart_send_fulfillment_tracking" id="smart-send-fulfillment-tracking">
    <Tabs>
      <Tab title="Description">
        **Filter** · `(bool $push, Smart_Send\Booking\Booked_Shipment $shipment, WC_Order $order) → bool`

        Runs after the order-note step, before forwarding parcel tracking to the optional WooCommerce Shipment Tracking plugin. Default: `true` outbound, `false` return. This does not create live tracking updates or install that plugin. Returning `false` leaves the booked shipment's own tracking data intact. The example disables forwarding.
      </Tab>

      <Tab title="Example">
        ```php theme={null}
        add_filter( 'smart_send_fulfillment_tracking', '__return_false' );
        ```
      </Tab>
    </Tabs>
  </Accordion>

  <Accordion title="smart_send_fulfillment_order_status" id="smart-send-fulfillment-order-status">
    <Tabs>
      <Tab title="Description">
        **Filter** · `(string|false $status, Smart_Send\Booking\Booked_Shipment $shipment, WC_Order $order) → string|false`

        Runs after tracking. Return a registered WooCommerce status such as `wc-completed`, or `false` to leave the order unchanged. Default: configured **Set order status after label print** for outbound; `false` for return. Updating status can trigger WooCommerce/third-party status hooks and emails. The example prevents Smart Send from changing status for either direction.
      </Tab>

      <Tab title="Example">
        ```php theme={null}
        add_filter( 'smart_send_fulfillment_order_status', '__return_false' );
        ```
      </Tab>
    </Tabs>
  </Accordion>

  <Accordion title="smart_send_order_fulfilled" id="smart-send-order-fulfilled">
    <Tabs>
      <Tab title="Description">
        **Action** · `(WC_Order $order, Smart_Send\Fulfillment\Fulfillment_Result $result) → void`

        Runs once after all attempted directions and their order-side steps, when at least one direction was fulfilled. A total failure does not fire it. It can carry outbound success alongside return failure. Inspect `get_outbound_shipment()`, `get_return_shipment()`, `shipments()`, `get_steps($shipment)`, `get_order_note_id($shipment)` and `get_warnings($shipment)`. `get_validation_errors($is_return)` and `get_error_details($is_return)` describe failures. See the [booked result contracts](#read-booked-results); do not assume every optional step ran.
      </Tab>

      <Tab title="Example">
        Use this event when you need saved note IDs, final document URLs or the outcome of both booking directions. The example records successful shipments and separately flags a failed return. It does not assume that every optional side effect was enabled.

        ```php theme={null}
        add_action( 'smart_send_order_fulfilled', function ( \WC_Order $order, \Smart_Send\Fulfillment\Fulfillment_Result $result ): void {
        	foreach ( $result->shipments() as $shipment ) {
        		$label = $shipment->label_document();
        		wc_get_logger()->info( 'Smart Send order updated', array(
        			'source'        => 'my-shop-shipping',
        			'order_id'      => $order->get_id(),
        			'shipment_id'   => $shipment->get_shipment_id(),
        			'note_id'       => $result->get_order_note_id( $shipment ),
        			'has_document'  => null !== $label,
        			'warning_count' => count( $result->get_warnings( $shipment ) ),
        		) );
        	}
        	if ( null !== $result->get_return_error() ) {
        		wc_get_logger()->warning( 'Return booking needs attention', array(
        			'source'   => 'my-shop-shipping',
        			'order_id' => $order->get_id(),
        		) );
        	}
        }, 10, 2 );
        ```

        **Expected result:** a combined success logs two shipments. An outbound success/return failure logs the outbound shipment and one warning; retry only the failed direction. If a local PDF copy fails, the shipment is still listed and `warning_count` increases. When using document URLs in your own integration, read `$label->download_url()` only after checking `$label` is not null. Do not log private document URLs. This event does not run when every attempted direction fails.
      </Tab>
    </Tabs>
  </Accordion>
</AccordionGroup>

### Environment, logging and plugin links

<AccordionGroup>
  <Accordion title="smart_send_api_endpoint" id="smart-send-api-endpoint">
    <Tabs>
      <Tab title="Description">
        **Filter** · `(string $host) → string`

        Runs when constructing the API client. Default host: `https://app.smartsend.io`. Return the scheme and hostname only, for example `https://app.smartsend.dev`; the client appends the API version path. Applies to authentication, pickup lookup and booking. Use credentials for the selected environment; a staging site alone does not change this host.
      </Tab>

      <Tab title="Example">
        Use an explicitly designated test shop and credentials for the Smart Send sandbox. Confirm the intended sandbox account and carrier setup before booking. The host filter affects token validation, pickup lookup and booking together.

        1. Set WordPress `WP_ENVIRONMENT_TYPE` to `staging` in the test shop's configuration.
        2. Add the callback below to your customization plugin.
        3. Enter and save the token for the sandbox environment, then validate it.
        4. Check checkout separately, then book a synthetic test order in that configured environment.

        The callback changes the host only for a staging environment. Do not append `/api/v1/` or another API path; the client adds it.

        ```php theme={null}
        add_filter( 'smart_send_api_endpoint', function ( string $host ): string {
        	if ( 'staging' !== wp_get_environment_type() ) {
        		return $host;
        	}
        	return 'https://app.smartsend.dev';
        } );
        ```

        **Expected result:** staging requests use `https://app.smartsend.dev` plus the client-managed API path; production retains the incoming host. Keep TLS certificate verification enabled. Remove the sandbox override and configure the intended production credentials when setting up a production shop. The plugin's automated-test mocks are test-suite fixtures, not a merchant-facing demo switch.
      </Tab>
    </Tabs>
  </Accordion>

  <Accordion title="smart_send_sslverify" id="smart-send-sslverify">
    <Tabs>
      <Tab title="Description">
        **Filter** · `(bool $verify) → bool`

        Default: `true`. Runs when preparing WordPress HTTP request options. Keep certificate verification enabled for production and sandbox. The example explicitly preserves verification; fix an invalid server certificate or CA configuration rather than disabling verification.
      </Tab>

      <Tab title="Example">
        ```php theme={null}
        add_filter( 'smart_send_sslverify', '__return_true' );
        ```
      </Tab>
    </Tabs>
  </Accordion>

  <Accordion title="smart_send_logging" id="smart-send-logging">
    <Tabs>
      <Tab title="Description">
        **Filter** · `(string $message, string $level, array $context) → string|null|false`

        Runs only for entries enabled by the plugin's logging policy. `$level` is a WooCommerce log level; `$context` includes `source` (`smart-send-logistics`) and plugin version. Return a rewritten message, or `null`/`false` to suppress the entry. This filter cannot enable disabled entries or replace the context by returning an array. The example suppresses debug messages only.
      </Tab>

      <Tab title="Example">
        ```php theme={null}
        add_filter( 'smart_send_logging', function ( string $message, string $level, array $context ) {
        	return 'debug' === $level ? null : $message;
        }, 10, 3 );
        ```
      </Tab>
    </Tabs>
  </Accordion>

  <Accordion title="smart_send_configuration_url" id="smart-send-configuration-url">
    <Tabs>
      <Tab title="Description">
        **Filter** · `(string $url) → string`

        Runs when building the Smart Send row's configuration guide link on the WordPress plugins screen. Default: `https://smartsend.io/woocommerce/configuration/`. Return a URL, not HTML. It does not change the API endpoint. The example routes the link to the local Smart Send settings screen.
      </Tab>

      <Tab title="Example">
        ```php theme={null}
        add_filter( 'smart_send_configuration_url', function ( string $url ): string {
        	return admin_url( 'admin.php?page=wc-settings&tab=shipping&section=smart_send_shipping' );
        } );
        ```
      </Tab>
    </Tabs>
  </Accordion>

  <Accordion title="smart_send_support_url" id="smart-send-support-url">
    <Tabs>
      <Tab title="Description">
        **Filter** · `(string $url) → string`

        Runs for the support link on the WordPress plugins screen. Default: `https://smartsend.io/support/`. Return a URL; the renderer escapes it. The example points to a support page on your own shop; create that page before using the snippet.
      </Tab>

      <Tab title="Example">
        ```php theme={null}
        add_filter( 'smart_send_support_url', function ( string $url ): string {
        	return home_url( '/shipping-help/' );
        } );
        ```
      </Tab>
    </Tabs>
  </Accordion>

  <Accordion title="ss_in_plugin_update_message" id="ss-in-plugin-update-message">
    <Tabs>
      <Tab title="Description">
        **Filter** · `(string $notice_html) → string`

        Runs when Smart Send displays a major-version update notice in the plugins list. Return trusted, safe HTML: the filtered result is rendered as HTML without another sanitization pass. The example appends static guidance. This is a notice hook, not a shipping/booking hook.
      </Tab>

      <Tab title="Example">
        ```php theme={null}
        add_filter( 'ss_in_plugin_update_message', function ( string $notice ): string {
        	return $notice . '<p>Review your shop customizations before updating.</p>';
        } );
        ```
      </Tab>
    </Tabs>
  </Accordion>
</AccordionGroup>

### Data contracts

The six order-reading filters above do not receive a return flag. They read the same order data for either direction. `smart_send_payload_totals` and `smart_send_shipment_freetext` run only when item data is non-empty. Use `smart_send_booking_request` for a final direction-specific adjustment.

<AccordionGroup>
  <Accordion title="Item rows" id="item-rows">
    `smart_send_payload_items` receives these keys on each row:

    | Keys                                          | Type and meaning                                                                 |
    | --------------------------------------------- | -------------------------------------------------------------------------------- |
    | `order_item_id`                               | Integer WooCommerce order-line identity; use it for allocation.                  |
    | `product_id`, `variation_id`                  | Integer catalog references; they do not identify an order line.                  |
    | `sku`, `name`                                 | Strings. A missing product has an empty SKU and the translated name **Deleted**. |
    | `description`, `hs_code`, `country_of_origin` | Product customs strings, or null when the product is unavailable.                |
    | `quantity`                                    | Order-line quantity; parcel allocation requires positive whole quantities.       |
    | `unit_weight`                                 | Float in kg, or null for an unavailable product.                                 |
    | `total_net_amount`, `total_tax_amount`        | Non-negative floats for the entire discounted line.                              |
    | `product_missing`                             | Boolean indicating an unavailable catalog product/variation.                     |
  </Accordion>

  <Accordion title="Totals" id="totals">
    `smart_send_payload_totals` receives floats `subtotal_net_amount`, `subtotal_tax_amount`, `shipping_net_amount`, `shipping_tax_amount`, `total_net_amount`, `total_tax_amount`, plus the string `currency`. Subtotal excludes shipping and includes non-shipping order fees. Ordinarily subtotal + shipping = total, separately for net and tax; clamping negative values can take precedence. Including-tax amounts are net + tax. Preserve those relationships if changing totals.
  </Accordion>
</AccordionGroup>

### Integration reference

This background describes platform hooks used by Smart Send, not additional public Smart Send customization hooks.

<AccordionGroup>
  <Accordion title="WooCommerce integration points" id="woocommerce-integration-points">
    The five WooCommerce-prefixed customization hooks above are intended for customizations. The table below explains relevant platform hooks that Smart Send **listens to**. Their broader contracts belong to WooCommerce; replacing the Smart Send callbacks is not the supported customization route.

    | Platform hook and arguments                                                                                                | How Smart Send uses it                                                                                                           |
    | -------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
    | `woocommerce_shipping_methods` — filter `(array $methods)`                                                                 | Registers the Smart Send shipping method; returns the method registry.                                                           |
    | `woocommerce_package_rates` — filter `(WC_Shipping_Rate[] $rates, array $package)`                                         | Sorts calculated rates when configured; returns rate objects keyed by rate ID. Use a later priority for your own final ordering. |
    | `woocommerce_after_shipping_rate` — action `(WC_Shipping_Rate $rate, int $index)`                                          | Renders pickup selection beside the selected rate in Classic Checkout.                                                           |
    | `woocommerce_after_checkout_validation` — action `(array $data, WP_Error $errors)`                                         | Validates Classic Checkout pickup selection and adds errors.                                                                     |
    | `woocommerce_checkout_create_order` — action `(WC_Order $order, array $data)`                                              | Adds the verified Classic Checkout pickup choice before order persistence.                                                       |
    | `woocommerce_store_api_checkout_update_order_from_request` — action `(WC_Order $order, WP_REST_Request $request)`          | Validates/persists the Checkout Block choice through the Store API.                                                              |
    | `woocommerce_order_details_after_order_table` — action `(WC_Order $order)`                                                 | Displays the saved pickup point on order details.                                                                                |
    | `woocommerce_email_after_order_table` — action `(WC_Order $order, bool $sent_to_admin, bool $plain_text, WC_Email $email)` | Displays the saved pickup point in HTML/plain-text email; the Smart Send callback consumes the first three arguments.            |
    | `woocommerce_product_options_shipping` — action `()`                                                                       | Adds customs fields to the product's Shipping tab.                                                                               |
    | `woocommerce_process_product_meta` — action `(int $product_id, WP_Post $post)`                                             | Saves submitted customs fields; the Smart Send callback consumes the product ID.                                                 |

    Smart Send also uses WooCommerce's Blocks registration and HPOS compatibility mechanisms. These registration details and the experimental Blocks data-attribute filter are implementation details; use the pickup filters above for shared behavior across checkout types.
  </Accordion>

  <Accordion title="Optional WooCommerce Subscriptions" id="optional-woocommerce-subscriptions">
    Smart Send uses these Subscriptions filters at priority 10:

    | Filter                                | Arguments and return                                                                 | Smart Send behavior                                                                                       |
    | ------------------------------------- | ------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------- |
    | `wc_subscriptions_renewal_order_data` | `(array $metadata) → array`                                                          | Excludes completed booking outcomes and copied tracking records.                                          |
    | `wcs_renewal_order_items`             | `(WC_Order_Item[] $items, WC_Order $renewal, WC_Subscription $subscription) → array` | Carries exact source item identities through item copying.                                                |
    | `wcs_renewal_order_created`           | `(WC_Order $renewal, WC_Subscription $subscription) → WC_Order`                      | Rebinds allocations to new order-item IDs and removes temporary markers; ambiguous plans require a reset. |

    Keep these filters' return values intact when adding your own Subscriptions callbacks. This integration follows the Subscriptions 4.9+ data-copy API; a test of the hook contract is not a test of the commercial plugin's scheduled renewal/payment engine. See [compatibility](/integrations/woocommerce/introduction#compatibility).
  </Accordion>

  <Accordion title="Verification scope" id="verification-scope">
    These snippets were executed against the plugin's real PHP value objects with isolated WordPress/WooCommerce test doubles. Checks cover plain-text labels, result order/limits, automatic versus explicit weight, colli allocation and outbound/return event results. They do not prove carrier acceptance, your other plugins' interactions or an external system's delivery. Verify the affected workflow on your configured test shop before deploying.
  </Accordion>
</AccordionGroup>

<span id="metafields" />

## Metadata

Smart Send stores different data on the order, its shipping items and the catalog products. The keys below describe those records; they are not interchangeable. Prefer the documented hooks and [PHP API](#api) when changing booking behavior. Use WooCommerce CRUD objects when inspecting metadata so your integration works with both HPOS and traditional order storage.

For order reads and intentional updates, follow [WooCommerce’s HPOS recipe book](https://developer.woocommerce.com/docs/features/orders/high-performance-order-storage/recipe-book/) <Icon icon="arrow-up-right" size={12} />: obtain the `WC_Order`, use its metadata methods, and save through the object. Do not assume orders are WordPress posts.

### Shipping-item configuration

These values belong to each `WC_Order_Item_Shipping`, not to `WC_Order`. WooCommerce copies them from the chosen Smart Send rate when it creates the shipping item.

| Key                                     | Stored value                          | Meaning                                                                                                                        |
| --------------------------------------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `smart_send_shipping_method`            | String, for example `postnord_agent`. | Outbound Smart Send service selected for this shipping method.                                                                 |
| `smart_send_return_method`              | Service-code string, or empty.        | Configured return service. A Smart Send shipping item without one does not have a configured return service.                   |
| `smart_send_auto_generate_return_label` | `'yes'` or `'no'`.                    | Recorded preference for including a return when booking outbound shipping. It does not book anything when the order is placed. |

These are snapshots of the method's configuration. Editing a zone's defaults does not rewrite existing shipping items. A service chosen for one booking in the order panel also does not overwrite these values. Read them with `$shipping_item->get_meta( $key, true )`; use `SS_SHIPPING_WC()->method_resolver()` when you need the resolved booking service, including the separate mapping for WooCommerce's native Free Shipping method.

### Order configuration and booking results

Read these fields from a `WC_Order` obtained with `wc_get_order()`. Delivery configuration is managed by `Smart_Send\Delivery\Order_Meta`; booking results are managed by `Smart_Send\Fulfillment\Shipment_IDs`.

| Key                            | Stored value                         | Meaning                                                                                                                                                                                  |
| ------------------------------ | ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ss_shipping_order_agent_no`   | Pickup-point number as a string.     | The selected pickup point's carrier-facing number. Keep it consistent with the stored point object.                                                                                      |
| `_ss_shipping_order_agent`     | Plain object with pickup-point data. | The saved pickup address and related data. Both checkout types save a server-verified point.                                                                                             |
| `ss_shipping_order_parcels`    | Array with a `specs` list.           | Reusable allocation of **order-item IDs** and quantities to colli. Explicit weights and dimensions are stored as `null`.                                                                 |
| `_ss_shipping_label_id`        | Smart Send shipment ID string.       | Latest successfully recorded **outbound** shipment.                                                                                                                                      |
| `_ss_shipping_return_label_id` | Smart Send shipment ID string.       | Latest successfully recorded **return** shipment.                                                                                                                                        |
| `_ss_shipping_labels`          | List of booking records.             | The newest 50 recorded shipments, stored oldest first within that list. Each row has `direction` (`outbound` or `return`), `shipment_id` (string) and `booked_at` (ISO 8601 UTC string). |

Shipment IDs identify Smart Send shipments; they are not tracking numbers, PDF URLs or WordPress attachment IDs. The history contains one row per shipment, not one per colli, and does not represent live carrier events. Outbound and return are recorded separately, including when outbound succeeds but the return fails. Use `SS_SHIPPING_WC()->shipment_ids()->get( $order, false )` for the latest outbound ID, `true` for return, or `labels( $order )` for the history. The ID reader returns an empty string when no ID is stored.

For the distinction between submitted values, filter changes and persistent data, see [delivery details and persistence](#delivery-details-and-persistence).

<AccordionGroup>
  <Accordion title="Pickup-point object" id="metadata-pickup-point">
    Known properties include `agent_no`, `company`, `address_line1`, `address_line2`, `postal_code`, `city` and `country`. A point may also contain `id`, `carrier`, `name_line1`, `name_line2`, `distance`, `coordinates` and `opening_hours`; do not assume every optional property is present. Country is an ISO 3166-1 alpha-2 code. Coordinates have `latitude` and `longitude`; opening-hour rows have `day`, `opens` and `closes`.

    Use `SS_SHIPPING_WC()->order_meta()->read( $order )->get_pickup_point()` for a `Smart_Send\Delivery\Pickup_Point` or `null`. The typed object supports accessors and `to_array()` without requiring callers to depend on every raw property. For booking changes, use `smart_send_delivery_details`; changing only the raw agent number is not a substitute for resolving and validating a compatible pickup point.
  </Accordion>

  <Accordion title="Stored colli allocation" id="metadata-parcel-plan">
    This example represents two units from order item `123` in one colli. That ID is local to the order; it is neither a product ID nor a variation ID.

    ```json theme={null}
    {
      "specs": [
        {
          "reference": "1",
          "weight": null,
          "length": null,
          "width": null,
          "height": null,
          "items": [
            { "order_item_id": 123, "quantity": 2, "name": "Cotton T-shirt" }
          ]
        }
      ]
    }
    ```

    `reference` and the optional item `name` are labels, not item identities. Quantities and order-item IDs are positive integers. `Parcel_Plan::from_array()` parses the canonical shape; allocation must also be checked against the current order's item IDs and quantities. Saving through `Order_Meta` strips the four measurement fields to `null`. An empty `specs` list means one parcel containing all items.

    Do not copy this array unchanged to another order. If items or quantities change, an incompatible saved allocation requires a reset before booking. See [delivery details and persistence](#delivery-details-and-persistence) for allocation rules and the measurements that apply only to a booking.
  </Accordion>

  <Accordion title="Renewals and fields owned by other integrations" id="metadata-renewals">
    The Subscriptions integration excludes `_ss_shipping_label_id`, `_ss_shipping_return_label_id` and `_ss_shipping_labels` from renewal metadata. It preserves reusable delivery configuration and remaps parcel allocations to the renewal's new order-item IDs. If that mapping cannot be verified, `ss_shipping_order_parcels` can contain an internal `unmapped_subscription_plan` wrapper that forces an explicit reset. This is an error marker, not a supported parcel-plan format to write.

    `_smart_send_renewal_source_item` is a temporary internal product-order-item marker with `order_id` and `item_id`; the integration removes it after processing the copied items. Do not use it as a persistent integration field.

    `_wc_shipment_tracking_items` belongs to WooCommerce Shipment Tracking. Smart Send uses that extension's API for tracking and excludes copied tracking records from renewals; it does not define the extension's metadata schema. `_vc_aio_options` belongs to vConnect and is read for compatibility when resolving delivery details. Neither field is a Smart Send-owned write contract.
  </Accordion>

  <Accordion title="Delivery details and persistence" id="delivery-details-and-persistence">
    A submitted order-screen value takes precedence over a stored value before `smart_send_delivery_details` runs. Following successful booking, the submitted pickup selection and parcel item allocations are saved. The submitted method, parcel weights and dimensions apply only to the current booking. Failed booking does not save those overrides. The filter's returned object determines what is booked, but it does not itself determine what is saved.

    For a combined outbound/return request, the submitted parcel plan applies to both directions unless a separate return plan overrides it. A null or empty plan means one parcel with all order items. A single spec with no allocations also includes all items and can supply explicit measurements. Multiple specs require complete allocation: every order unit exactly once, positive whole quantities, and no duplicate order-item row within a spec. Unknown order-item IDs and under-/over-allocation are rejected.
  </Accordion>
</AccordionGroup>

### Product customs information

These fields belong to a `WC_Product` or `WC_Product_Variation`, not an order or shipping item.

| Key                     | Stored value                           | Meaning                                                                             |
| ----------------------- | -------------------------------------- | ----------------------------------------------------------------------------------- |
| `_ss_country_of_origin` | Country-code string, for example `DK`. | ISO 3166-1 alpha-2 country where the goods were produced.                           |
| `_ss_customs_desc`      | Text string.                           | Description of the goods for customs.                                               |
| `_ss_hs_code`           | Text string.                           | HS/tariff classification. Preserve it as text rather than casting it to an integer. |

Use `wc_get_product()` and the product's `get_meta()` for reading. To save an intentional product-data change, use `update_meta_data()` followed by the product's `save()`; setting a value does not determine whether it is the correct customs classification.

Booking reads the current catalog values. A variation's value takes precedence unless it is `''` or `null`, in which case the parent product's value is used. The plugin's product editor supplies the fields on the main product's Shipping tab; it does not add a separate editor for each variation. These fields are not snapshots copied onto order lines. A deleted catalog product cannot supply its customs data. See the [merchant guide to customs fields](/integrations/woocommerce/shipping-labels#customs-information).

## API

Use these PHP APIs inside WordPress to read Smart Send data for an existing `WC_Order`. Smart Send and a supported WooCommerce version must be active. Call the examples after Smart Send initializes on `init` at priority `0`, for example from an `init` callback at priority `10` or a later order hook. If you start with an order ID, load it with `wc_get_order($order_id)` and check that the result is a `WC_Order`.

These reads use WooCommerce's order and item APIs, so they work with HPOS. They do not book shipments or contact the Smart Send API. The example functions use a `my_shop_` prefix; choose your own prefix in your integration.

<AccordionGroup>
  <Accordion title="Read the order's pickup point" id="get-order-pickup-point">
    <Tabs>
      <Tab title="Description">
        `SS_SHIPPING_WC()->order_meta()->read($order)` returns `Smart_Send\Delivery\Delivery_Details` with the stored pickup point and parcel plan. `get_pickup_point()` returns a `Smart_Send\Delivery\Pickup_Point` or `null`. See the [pickup point accessors](#pickup-point-accessors) for the available fields.

        The helper reads the saved point; it does not find nearby points or refresh its address. If no Smart Send point is stored, it also recognizes the supported vConnect pickup point metadata. An agent number stored on its own does not provide a complete pickup point object. Individual fields can be `null`.

        `get_parcel_plan()` on the same details object returns the saved plan or `null`. To distinguish a missing plan from an invalid saved allocation, use `SS_SHIPPING_WC()->order_meta()->parcel_plan_error($order)`, which returns an explanation or `null`. These stored details do not resolve a shipping method; use the next example for that.
      </Tab>

      <Tab title="Example">
        Return the saved point's identity and address, or `null` when no point is available. Escape individual values for the output context if you render them in HTML.

        ```php theme={null}
        function my_shop_get_order_pickup_point( \WC_Order $order ): ?array {
        	$point = SS_SHIPPING_WC()->order_meta()->read( $order )->get_pickup_point();

        	if ( null === $point ) {
        		return null;
        	}

        	return array(
        		'agent_no'      => $point->get_agent_no(),
        		'carrier'       => $point->get_carrier(),
        		'company'       => $point->get_company(),
        		'address_line1' => $point->get_address_line1(),
        		'address_line2' => $point->get_address_line2(),
        		'postal_code'   => $point->get_postal_code(),
        		'city'          => $point->get_city(),
        		'country'       => $point->get_country(),
        	);
        }
        ```
      </Tab>
    </Tabs>
  </Accordion>

  <Accordion title="Read shipping methods and booking defaults" id="get-order-shipping-methods">
    <Tabs>
      <Tab title="Description">
        There are two different IDs to work with:

        * `$order->get_shipping_methods()` returns the WooCommerce shipping line items saved on the order. `get_method_id()` identifies the WooCommerce method, such as `smart_send_shipping` or `free_shipping`; `get_instance_id()` identifies the shipping-zone method instance.
        * `SS_SHIPPING_WC()->method_resolver()` resolves one Smart Send service ID for each direction, such as `postnord_agent` or `postnord_returndropoff`. It reads the order's Smart Send shipping-line metadata, the current [mapping for native free shipping](/integrations/woocommerce/shipping-methods#native-free-shipping), or supported third-party method mappings.

        `resolve_outbound($order)` and `resolve_return($order)` return `''` when no service can be resolved. If a Smart Send shipping line has no return method configured, `resolve_return()` throws `Smart_Send\Booking\Exceptions\Booking_Exception`; the example keeps this distinction in `return_error`. `is_auto_return_enabled($order)` reads the automatic-return setting saved on the shipping line.

        These are the defaults derived for the order. They do not include unsaved choices in the order screen, submitted booking overrides, or changes made by `smart_send_delivery_details`. They also do not tell you which service a previous shipment was actually booked with; read that from the `Booked_Shipment` delivered to a [booking action](#smart-send-booking-completed).
      </Tab>

      <Tab title="Example">
        Return both the WooCommerce shipping lines and the resolved Smart Send defaults. The return error is plain text; escape it before displaying it in HTML.

        ```php theme={null}
        function my_shop_get_order_shipping_methods( \WC_Order $order ): array {
        	$shipping_lines = array();

        	foreach ( $order->get_shipping_methods() as $item_id => $item ) {
        		$shipping_lines[] = array(
        			'order_item_id' => (int) $item_id,
        			'method_id'     => $item->get_method_id(),
        			'instance_id'   => $item->get_instance_id(),
        			'title'         => $item->get_method_title(),
        		);
        	}

        	$resolver      = SS_SHIPPING_WC()->method_resolver();
        	$return_method = '';
        	$return_error  = null;

        	try {
        		$return_method = $resolver->resolve_return( $order );
        	} catch ( \Smart_Send\Booking\Exceptions\Booking_Exception $exception ) {
        		$return_error = $exception->getMessage();
        	}

        	return array(
        		'shipping_lines' => $shipping_lines,
        		'outbound'       => $resolver->resolve_outbound( $order ),
        		'return'         => $return_method,
        		'return_error'   => $return_error,
        		'auto_return'    => $resolver->is_auto_return_enabled( $order ),
        	);
        }
        ```
      </Tab>
    </Tabs>
  </Accordion>

  <Accordion title="Read saved bookings" id="get-order-bookings">
    <Tabs>
      <Tab title="Description">
        `SS_SHIPPING_WC()->shipment_ids()` reads the shipment references saved on the order:

        | Method               | Result                                                                                                                              |
        | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
        | `get($order, false)` | Latest saved outbound shipment ID, or `''`.                                                                                         |
        | `get($order, true)`  | Latest saved return shipment ID, or `''`.                                                                                           |
        | `labels($order)`     | Saved history rows, oldest first; each contains `direction`, `shipment_id` and `booked_at`. Missing history returns an empty array. |

        `direction` is `outbound` or `return`; `booked_at` is an ISO 8601 UTC timestamp or `null` when unavailable. The plugin retains the latest 50 history rows in total across both directions. The latest ID for each direction is stored separately, so an ID can exist even when `labels()` is empty. A row represents one shipment, which may contain several parcels.

        These are saved booking references. They do not contain live carrier status, tracking links, documents or the complete `Booked_Shipment`. To read documents and tracking details returned by a booking, listen to [smart\_send\_booking\_completed](#smart-send-booking-completed) or [smart\_send\_order\_fulfilled](#smart-send-order-fulfilled) and use the [booked result accessors](#read-booked-results). A `Fulfillment_Result` describes that particular run, including a possible outbound success and return failure; it is not re-created from the order history.
      </Tab>

      <Tab title="Example">
        Read the latest shipment ID for each direction and the retained history independently. Do not assume a successful outbound booking also produced a return shipment.

        ```php theme={null}
        function my_shop_get_order_bookings( \WC_Order $order ): array {
        	$shipments = SS_SHIPPING_WC()->shipment_ids();

        	return array(
        		'latest_outbound_id' => $shipments->get( $order, false ),
        		'latest_return_id'   => $shipments->get( $order, true ),
        		'history'           => $shipments->labels( $order ),
        	);
        }
        ```
      </Tab>
    </Tabs>
  </Accordion>

  <Accordion title="Pickup-point accessors" id="pickup-point-accessors">
    `Pickup_Point` supplies `get_agent_no()`, `get_company()`, `get_address_line1()`, `get_address_line2()`, `get_postal_code()`, `get_city()`, `get_country()`, `get_carrier()`, `get_distance()`, `get_latitude()`, `get_longitude()` and `get_opening_hours()`. Optional values can be null. Opening hours are a list of `day`, `opens`, `closes` rows: the day is a lowercase English weekday and times use `HH:MM:SS`. An empty list means unknown opening hours. `to_array()` gives a serializable representation; it is not permission to trust client-submitted names or addresses.
  </Accordion>

  <Accordion title="Read booked results" id="read-booked-results">
    `Booked_Shipment` exposes `get_shipment_id()`, `get_carrier()`, `get_service_code()`, `is_return()`, `get_state()`, `get_booked_at()`, `get_tracking_code()`, `get_tracking_url()`, `parcels()`, `documents()` and `codes()`. Optional tracking fields can be null. `label_document()` returns a `Shipment_Document` or null; do not assume a PDF exists for every result. A document's `download_url()` chooses the saved local copy when available, otherwise the Smart Send URL.

    `Fulfillment_Result::get_steps($shipment)` reports `save_documents` (`true`, `false` or `'failed'`), `order_note` (boolean), `tracking` (whether forwarding was requested) and `order_status` (status or `false`). A tracking step does not prove that an optional third-party integration was installed or delivered an email.

    Use `get_order_note_id($shipment)` for the saved note's integer ID or null. `get_order_note($shipment)` supplies note HTML for PHP consumers. The serializable `to_array()` instead contains an `order_note` object with an `id`, not note HTML. Its rows have `direction`, `status` (`fulfilled`/`failed`), and shipment/steps/warnings or an error. `get_error_details($is_return)` returns null or an array with `message`, `response_id`, `fields` and `html`; treat `message` as plain text and escape output appropriately.
  </Accordion>
</AccordionGroup>
