# Migrating from Ecolet — overview

You integrate with the **Ecolet API** (`panel.ecolet.ro`, v1/v2) and your
shipments are moving to the **Apaczka** platform. This section maps what you
already have onto the Apaczka API v2 so you can rewrite the transport layer
without rediscovering every difference yourself.

Three pages:

1. **This overview** — the concepts that change, and in what order to tackle them.
2. [Endpoint mapping](/migration/ecolet-endpoint-map) — call by call, what replaces what.
3. [Gaps and workarounds](/migration/ecolet-gaps) — what Apaczka has no equivalent for, and what to do instead.

:::note[Your business logic survives; the protocol does not]

Orders, parcels, COD, labels and tracking all exist on both sides. What
changes is *how* you talk to the API — authentication, transport, the shape
of an address, and the fact that order creation is synchronous. Budget the
work there.

:::

## The five differences that drive the work

| # | Area | Ecolet | Apaczka API v2 |
| --- | --- | --- | --- |
| 1 | Authentication | Bearer JWT from `oauth/token` | Per-request **HMAC-SHA256** signature |
| 2 | Transport | GET / POST / DELETE, JSON body, HTTP 401/422/500 | **POST only**, form-urlencoded envelope, **always HTTP 200** |
| 3 | Order creation | Asynchronous — `send-order` → `order_to_send_id` → polling | **Synchronous** — one call returns the order and its waybill number |
| 4 | Money | RON, decimal strings (`"16,28"`) | Integers in **minor units** (grosze: 1 PLN = 100 gr) |
| 5 | Addresses | Structured RO fields + `locality_id` | `line1` / `line2` + free-text `city` + `postal_code` |

Everything else — labels, multi-parcel shipments, COD, pickup windows — is a
mechanical transformation.

## 1. Authentication — no tokens, sign every request

There is no login call and no session. Each request carries your `app_id`,
an `expires` timestamp and an HMAC-SHA256 `signature` computed over the
route, the payload and the expiry, using your `app_secret`.

```text
Ecolet    POST /oauth/token  →  access_token  →  Authorization: Bearer …
Apaczka   sign(app_secret, "order_send/:{json}:{expires}")  →  every request
```

Practical consequences:

- **Delete your token cache and refresh logic.** There is nothing to store
  or renew.
- **`expires` is a short validity window per request**, not a session TTL.
  Your server clock matters — skew causes mass rejections.
- The `app_secret` never leaves your server. Signing in a browser or mobile
  app leaks it.

Full algorithm with worked examples: [Request signing
(HMAC)](/legacy/hmac-signing). Using PHP? The [SDK](/sdk) signs for you and
this whole section becomes two constructor arguments.

## 2. Transport — one verb, one status code

Every endpoint is a `POST` to a path with a **trailing slash**, with the
payload sent as a JSON string inside a form field:

```text
app_id=<uuid>&request=<json>&expires=<unix>&signature=<hmac>
```

The response is **always HTTP 200**. The real outcome lives in the body:

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

So:

- Rewrite `GET /order/{id}` as `POST order/{id}/`, `DELETE /order/{id}` as
  `POST cancel_order/{id}/`.
- **Stop branching on HTTP status codes.** Branch on the body's `status`.
  Any HTTP code other than 200 is a transport failure, not an API error.
- Ecolet's field-level `errors[]` has no direct counterpart — errors arrive
  as a single `message`. If your UI highlights individual invalid fields,
  see [Gaps and workarounds](/migration/ecolet-gaps#field-level-validation-errors).

Details: [Error semantics](/legacy/error-semantics).

## 3. Order creation — synchronous, and not idempotent

Ecolet's flow was: `send-order` → `order_to_send_id` → poll
`order-to-send/{id}` until `ordered` or `error` → then read the order.

Apaczka's `order_send` does all of it in one call and returns the order id,
the carrier waybill number and the tracking URL. The
`order_to_send` state machine disappears — there is no intermediate entity,
nothing to poll, and no `order_to_send/destroy`.

:::caution[Remove the polling loop, add retry discipline]

`order_send` is **not idempotent**. Where Ecolet let you re-poll a pending
job safely, a retried Apaczka call after a timeout can create a **second
order and a second waybill**. Treat a timeout as "unknown" and reconcile via
the order list — never as "failed, retry". See [best
practices](/guides/best-practices#be-careful-with-retries).

:::

The full flow, end to end: [Order lifecycle](/guides/order-lifecycle).

## 4. Money — integers, minor units

| | Ecolet | Apaczka |
| --- | --- | --- |
| Type | decimal string, comma separator (`"16,28"`) | integer |
| Unit | RON | minor units — grosze in the reference (1 PLN = 100 gr) |
| Fee breakdown | itemised `fees[]` | net / gross totals |

Convert at the edges of your system and keep integers internally. `1628`
means 16.28 — an off-by-100 here becomes a real invoice.

COD amounts and `shipment_value` follow the same rule; `cod.currency`
carries the currency code explicitly.

:::note[Confirm currency and VAT handling before go-live]

The RON ↔ PLN question (which currency your account is settled in, and
whether COD payouts convert) is a commercial setting, not an API flag.
Confirm it with your Alsendo contact rather than inferring it from a test
response. VAT differs between markets and is an invoicing concern, not an
API one.

:::

## 5. Addresses — two lines, not eight fields

Ecolet models a Romanian address as discrete fields. Apaczka takes two free
address lines plus city, postal code and country:

| Ecolet | Apaczka | How |
| --- | --- | --- |
| `street_name`, `street_number` | `line1` | concatenate |
| `block`, `entrance`, `floor`, `flat` | `line2` | concatenate with prefixes |
| `locality_id` | `city` | resolve the id to a name (see [gaps](/migration/ecolet-gaps#locations-api)) |
| `locality` | `city` | direct |
| `county` | `state_code` | direct |
| — | `name`, `contact_person`, `email`, `phone` | required contact fields |

:::caution[The join is lossy — keep your own copy]

`line1` + `line2` cannot be reliably split back into block / entrance /
floor / flat. Romanian couriers often want those parts separately, so
**store the structured address in your own database** and treat the Apaczka
representation as a rendering of it, not as the record.

:::

When composing `line2`, use the conventional Romanian prefixes so a human
(and the courier) can still parse it:

```text
line2 = "bl. 12, sc. A, et. 3, ap. 27"
        bloc      scara   etaj    apartament
```

Watch the field length limits in the [API v2
reference](/reference/v2-legacy) — a long Romanian address can exceed them.

Parcel points (lockers, PUDO) are attached differently too: the point id
goes into `foreign_address_id` on the relevant address and the pickup type
stays `SELF`. See [Pickup types](/guides/pickups).

## Suggested order of work

Each step is independently testable, and the earlier ones unblock the later
ones:

1. **Signing** — get one `service_structure` call to return `status: 200`.
   Nothing else works until this does.
2. **Service dictionary** — map your Ecolet service slugs onto the numeric
   `service_id`s your account actually offers. Do not hardcode the list;
   read it from `service_structure` and cache it (refresh daily).
3. **Address rendering** — build and unit-test the Ecolet → `line1`/`line2`
   transformation, including the `locality_id` lookup.
4. **Order creation** on the [sandbox](/guides/testing) — `order_send`, then
   `waybill`. At this point you can ship.
5. **Tracking** — switch from Ecolet's batch status call to per-waybill
   `tracking`, or (better) to [push webhooks](/guides/tracking).
6. **The rest** — COD, options, parcel points, pickups, cancellation.
7. **The gaps** — locations, address book, `/me` usage. Read
   [Gaps and workarounds](/migration/ecolet-gaps) *before* step 1 so none of
   them surprise you at the end.

---

**Next:** [Endpoint mapping](/migration/ecolet-endpoint-map) ·
[Gaps and workarounds](/migration/ecolet-gaps) ·
[Quick start](/quick-start)
