# Choosing a service from the offer

Most integrations already know which carrier service they want and pass a
fixed `service_id`. When you don't — when you want to present the customer
(or your own rules engine) the set of services that can actually carry
**this** parcel and pick one — price the shipment across all services first,
then choose against the full set of constraints, **not price alone**.

The cheapest quote is often the wrong pick: a point-to-point service needs a
locker id, an international one needs customs data, some services require an
ordered courier pickup. A service you can't satisfy fails at `order_send`,
not at valuation.

This guide covers the **front** of the flow. Everything after order creation
(waybill, tracking, cancellation) is identical to the
[Order lifecycle](/guides/order-lifecycle) — this page only replaces the way
the service is selected.

`order_valuation` (no `service_id`) → `service_structure` (map requirements) →
enrich payload (pickup / point / customs) → `order_send` →
waybill / tracking / … (see [Order lifecycle](/guides/order-lifecycle))

Examples use the [PHP SDK](/sdk); each step names the underlying API method,
so the flow maps 1:1 to raw [envelope requests](/legacy/hmac-signing) from
any language.

## 1. Price across services — `order_valuation` without a service

Build the order on a **neutral skeleton** — `SELF` pickup, no point, no
customs — and **omit the service**. `order_valuation` then returns a
`price_table` with every service that can carry this parcel.

```php
use AlsendoOne\SDK\DTO\Address;
use AlsendoOne\SDK\DTO\Request\OrderRequest;
use AlsendoOne\SDK\DTO\Request\PickupRequest;
use AlsendoOne\SDK\DTO\Request\ShipmentRequest;

$shipment = new ShipmentRequest('PACZKA', 2.5, 40, 30, 20); // kg, cm

$order = OrderRequest::create()
    ->setSenderAddress(new Address(/* ... */))
    ->setReceiverAddress(new Address(/* ... */))
    ->addShipment($shipment)
    ->setPickup(new PickupRequest('SELF'));   // neutral — no setService()

$valuation = $client->getValuation($order);

foreach ($valuation->getPriceTable() as $price) {
    // Prices come back in groszy (1 PLN = 100 gr)
    printf("%d: %.2f PLN gross\n", $price->getServiceId(), $price->getPriceGross() / 100);
}
```

Without the SDK — same call, no `service_id` in the payload:

```bash
--data-urlencode 'request={"order":{"address":{"sender":{...},"receiver":{...}},"pickup":{"type":"SELF"},"shipment":[{"dimension1":40,"dimension2":30,"dimension3":20,"weight":2.5,"is_nstd":0}]}}'
# POST order_valuation/  → response.price_table, one entry per service_id, amounts in groszy
```

:::tip[Price the skeleton, not the final order]

Adding service-specific fields (a point, a courier pickup, customs) can
change or restrict what's available. Quote first on the neutral order, enrich
only **after** you choose.

:::

## 2. Map the offer to `service_structure` — check the requirements

`price_table` tells you **who** can carry it and for how much.
`service_structure` tells you **what each service demands**. Join them by
`service_id`. Read the dictionary from cache (refresh about once a day)
rather than calling it on every valuation — see
[Best practices](/guides/best-practices#dont-hardcode-carriers-and-services).

```php
$structure = $client->getServiceStructure();

$byId = [];
foreach ($structure->getServices() as $svc) {
    $byId[(int) $svc['service_id']] = $svc;
}

$offer = [];
foreach ($valuation->getPriceTable() as $price) {
    $svc = $byId[$price->getServiceId()] ?? null;
    if ($svc === null) {
        continue; // priced but not in the current dictionary — skip
    }

    $offer[] = [
        'service_id'     => $price->getServiceId(),
        'name'           => $svc['name'],
        'supplier'       => $svc['supplier'],
        'price_gross'    => $price->getPriceGross(),   // groszy
        'domestic'       => (int) $svc['domestic'],        // 1 domestic / 0 international
        'pickup_courier' => (int) $svc['pickup_courier'],  // 0 unavailable / 1 available / 2 required
        'door_to_door'   => (int) $svc['door_to_door'],
        'door_to_point'  => (int) $svc['door_to_point'],
        'point_to_point' => (int) $svc['point_to_point'],
        'point_to_door'  => (int) $svc['point_to_door'],
    ];
}
```

What each flag means for the payload you'll build in step 3:

| Service condition (from `service_structure`) | What you must add before `order_send` |
|---|---|
| `pickup_courier = 2` (courier **required**) | `pickup.type = COURIER` + `date` / `hours_from` / `hours_to` — pick a window from `pickup_hours` |
| `pickup_courier = 0` (courier unavailable) | `SELF` / drop-off only — no courier pickup |
| point routing (`door_to_point` / `point_to_point` / `point_to_door` = 1) | a `foreign_address_id` from [`points/{type}/`](/reference/v2-legacy) on the relevant side (sender and/or receiver) |
| `domestic = 0` (international) | customs: `customs_data` per shipment item (+ the top-level `customs` block where the service requires it) |
| COD / options wanted | check `options` in the dictionary, and `option_cod` / `option_send` / `option_deliver` on the chosen point |

:::caution[Cheapest ≠ best]

Choose against your own policy — SLA, routing, and whether you can actually
meet the extra requirements — not the lowest `price_gross`. Filtering the
offer down to *fulfillable* services is the whole point of this flow.

:::

## 3. Enrich the payload for the chosen service

Once chosen, set the service and add **only** the fields that service needs.

```php
use AlsendoOne\SDK\DTO\Request\CustomsDataRequest;
use AlsendoOne\SDK\DTO\Request\PickupRequest;
use AlsendoOne\SDK\Type\Service;

$serviceId = /* your pick from $offer */;
$svc       = $byId[$serviceId];
$service   = Service::from($serviceId);

$order->setService($service);

// Courier pickup required → schedule it from an available window
if ((int) $svc['pickup_courier'] === 2) {
    $days = $client->getPickupHours($senderPostalCode, $service)->getHours();
    $day  = reset($days);
    $order->setPickup(new PickupRequest('COURIER', $day->getDate(), '09:00', '15:00'));
}

// Point routing → attach the point via foreign_address_id on the receiver
// (pick the point id from points/<type>/). Address is immutable — build a new one.
if ((int) $svc['point_to_point'] === 1 || (int) $svc['door_to_point'] === 1) {
    $order->setReceiverAddress(new Address(
        name: 'Jan Kowalski',
        phone: '+48600000000',
        email: 'jan@example.com',
        postalCode: '00-001',
        city: 'Warszawa',
        countryCode: 'PL',
        foreignAddressId: $pointId,          // e.g. an InPost locker id
    ));
}

// International → declare customs per item
if ((int) $svc['domestic'] === 0) {
    $shipment->addCustomsData(new CustomsDataRequest(
        'Cotton T-shirt', 3, 1550, 1, '6109100010', 'PL' // desc, qty, unit price (gr), weight, HS code, origin
    ));
}
```

The top-level `customs` block (`content_type`, `EORI_number`,
`documents_form`, `documents`) has no typed builder yet. If the service
requires it, convert the order with `toArray()`, add the block and pass the
array to `sendOrder()` — it accepts both.

:::caution[Pickup naming trap]

At order creation the fields are plural — `hours_from` / `hours_to`. The
standalone `pickup` / `batch_pickup` endpoints use the singular `hour_from` /
`hour_to`. See [Pickup types](/guides/pickups).

:::

Enriching the order can change its price — if you showed the first quote to
the customer, re-price the final payload before sending (see
[Order lifecycle → step 2b](/guides/order-lifecycle#2b-optional-re-price-the-final-payload--order_valuation)).

## 4. Create the order — `order_send`

```php
$created = $client->sendOrder($order);

$created->getId();            // Apaczka order id
$created->getWaybillNumber(); // carrier tracking number
$created->getStatus();        // e.g. NEW
```

:::caution

`order_send` is **not idempotent** — a retried timeout can create a
duplicate order. Reconcile with `getOrders()` before retrying, never
blind-retry a write. See
[best practices](/guides/best-practices#be-careful-with-retries).

:::

## 5. Continue as usual

From here the flow is identical to the [Order lifecycle](/guides/order-lifecycle):
download the waybill, optionally the dispatch code / turn-in, then track the
shipment ([Tracking](/guides/tracking)). Cancellation and order inspection
are unchanged.

---

**See also:** [Order lifecycle](/guides/order-lifecycle) ·
[Pickup types](/guides/pickups) · [Tracking](/guides/tracking) ·
[Best practices](/guides/best-practices) ·
[Request signing (HMAC)](/legacy/hmac-signing)
