# Gaps and workarounds

Some of what the Ecolet API gave you has no counterpart in the Apaczka API
v2. None of it is a blocker, but each one is a decision you should make
**before** you start rewriting, not after — most of them mean data or state
moves into your own system.

## Locations API

**Gone:** locality, street, postal-code and county lookups
(`locations/ro/localities/{code}`, `locations/{id}/streets/{street}`,
`search-street-postal-codes`, `counties`, `countries`).

**Why it matters:** Ecolet addresses are built around `locality_id`, an id
from Ecolet's own dictionary. Apaczka takes a free-text `city` and a
`postal_code` — there is no dictionary to query and no id to resolve.

**What to do:**

1. **Export the locality dictionary while you still have Ecolet access.**
   You need at minimum `locality_id → name, county, postal codes`. Without
   it you cannot render historical addresses.
2. **Own it going forward.** Romanian postal data is available from public
   sources; refresh it on a schedule. This is the single largest piece of
   work created by the migration — budget for it explicitly.
3. **Keep validating on your side.** Apaczka will accept an address it
   cannot deliver to; a bad `city`/`postal_code` pair surfaces as a carrier
   failure, not an API error.

:::caution[Do this first]

Address autocompletion is usually the most-called part of a shipping
integration. If your checkout depends on street search, that feature has to
exist on your side before you can cut over — it is not something to
discover in the last sprint.

:::

## Address book

**Gone:** `v1/address-book`.

**What to do:** keep addresses in your own database. If you were using
Ecolet as the system of record for sender/receiver addresses, export them
before cut-over. Apaczka takes the full address on every order, so nothing
is lost functionally — only the storage moves.

## Order basket / async validation

**Gone:** `order-to-send/{id}` and `order-to-send/destroy`, and with them
the `new` / `ordered` / `error` state machine.

**Why it matters:** in Ecolet, `send-order` returned an `order_to_send_id`
and you polled until the order materialised. That intermediate entity was
also where you learned about validation problems.

**What to do:**

- Drop the polling loop and the intermediate state entirely — `order_send/`
  returns the finished order or an error envelope.
- If you show users a "ready to send" basket, keep it **local**. Apaczka has
  no concept of a draft order.
- To check a payload before committing, call `order_valuation/` with the
  final order object. It fails on the same structural problems and also
  gives you the price.

## Field-level validation errors

**Gone:** Ecolet's `422` with `errors: { "postal_code": ["…"] }`.

Apaczka returns a single message:

```json
{ "status": 400, "message": "Validation error", "response": null }
```

**What to do:** validate on your side where the user can see it — required
fields, postal-code format, phone format, field lengths, option
availability. Treat the API's `message` as a last-resort diagnostic to log
and show to support staff, not as something to map back onto form fields.
See [Error semantics](/legacy/error-semantics).

## Batch tracking

**Gone:** `order/get-statuses-for-many-orders` — one call, many orders.

`tracking/{waybill}/` handles **one waybill per call**.

**What to do:**

- **Preferred:** switch to [push tracking
  webhooks](/guides/tracking#push-tracking-webhooks). Status changes arrive
  as signed notifications and the polling problem disappears.
- If you must poll: only poll orders that are actually in transit, back off
  as a shipment ages, and cache. Naïvely fanning out one request per open
  order per minute will not scale.

## Parcel point lookup

`points/{type}/` returns points by type / carrier, not by locality, and it
has no `destination` (`sender` / `receiver`) filter. Romanian lockers
frequently differ in whether they accept drop-offs, deliveries or both.

**What to do:** read `option_send`, `option_deliver` and `option_cod` on
each point and filter yourself before offering it to the user. Cache the
directory (it changes slowly) and select on your side by coordinates or
postal code.

Point ids are Apaczka's, not Ecolet's — if you stored Ecolet point ids on
past orders, keep them, but resolve fresh ids for new ones.

## COD bank account

**Different:** Ecolet always paid COD out to the account configured on the
user. Apaczka expects the payout account on the order — either
`cod.bankaccount` or `cod.bankaccount_id` referring to an account saved in
the panel.

**What to do:** save the payout account in the Apaczka panel once and send
`cod.bankaccount_id`. That reproduces the Ecolet behaviour without putting
account numbers in every request.

## Account and credit information

`me/` exists but is **feature-flag-gated** and does not reproduce Ecolet's
`v1/me` — credit limit, credit usage, unpaid invoices, blocked carriers and
services are not part of it.

**What to do:** if your integration gates order creation on the customer's
credit standing, raise it with your Alsendo contact before cut-over. Do not
build against `me/` until you have confirmed it is enabled on your account
and returns what you need.

## Smaller differences

| Area | Difference | What to do |
| --- | --- | --- |
| Labels | Base64 inside JSON, not a binary stream | `base64_decode()` before writing the file |
| Label format | `is_zebra` is a request field; in Ecolet it came from user settings | Pass the format explicitly per request |
| Multi-parcel | `parcel.amount` / `parcels` object | One `shipment[]` entry per physical parcel |
| Pickup hours | Ecolet takes a single hour, Apaczka a range | Send `hours_from` / `hours_to`; read valid windows from `pickup_hours/` |
| Coupon codes | No equivalent | Discounts are a commercial arrangement on the account, not an API field |
| `uit_code` | No equivalent | Keep as your own metadata (ANAF high-tax-risk goods) |
| `sender_forklift` / `receiver_forklift` | No equivalent | Pallet-specific; confirm handling per service with your Alsendo contact |
| Rate limiting | Different model and limits | Cache `service_structure/` and `points/` (refresh daily) and `pickup_hours/` (minutes). A shared cache — Redis or equivalent — is the practical answer for multi-worker setups. |

## Migration checklist

Before cut-over, make sure you have:

- [ ] Exported the Ecolet locality dictionary and any address book data
- [ ] A street / postal-code lookup running on your side
- [ ] A mapping table from Ecolet service slugs to Apaczka `service_id`s,
      built from `service_structure/` and refreshed, not hardcoded
- [ ] A mapping for additional services, with an explicit decision for the
      ones that have no equivalent
- [ ] Structured addresses stored on your side — the `line1`/`line2` join is
      one-way
- [ ] Money handled as integer minor units end to end
- [ ] The `order_to_send` polling loop removed and retry behaviour reviewed
      (`order_send/` is not idempotent)
- [ ] Tracking moved to webhooks, or polling reduced to open shipments only
- [ ] The COD payout account configured in the Apaczka panel
- [ ] Validation moved to your side, with API `message` values logged
- [ ] The whole flow exercised on the [sandbox](/guides/testing)

---

**See also:** [Overview](/migration/ecolet-overview) ·
[Endpoint mapping](/migration/ecolet-endpoint-map) ·
[Best practices](/guides/best-practices)
