> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.milenow.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.milenow.com/_mcp/server.

# Authentication, rate limits and errors

## Base URL

```
https://lastmile.milenow.com/index.php
```

All paths below start with `/api/v1/partners`.

## Company login

Log in with your company API user. This is the existing [login endpoint](/api-reference/login/post-app-apiv-1-trackingapi-index).

```bash
curl -X POST "https://lastmile.milenow.com/index.php/api/v1/partners/login" \
  -d "user=YOUR_API_USER" \
  -d "password=YOUR_PASSWORD"
```

```json
{
  "status": 200,
  "msg": "",
  "msgs": [],
  "data": {
    "expiry_time": "2026-10-28 16:11:22",
    "error_msg": ""
  },
  "access_token": "<company-token>"
}
```

* The token is in `access_token` at the **top level** of the response, not inside `data`.
* A token lasts 90 minutes.
* While your token is still valid, logging in again returns the **same** token with `error_msg` set to `Your Previous Token Already Exist. Within 90 minutes only one token is allowed`. The status is 200. This is not an error.
* Keep the token and reuse it. Log in again when a call returns 401 `Your token expired.`

## Transporter login

Transporters log in with the email or username and password they use on the Mile web app. Owners and team members can both log in.

```bash
curl -X POST "https://lastmile.milenow.com/index.php/api/v1/partners/transporters/login" \
  -H "Content-Type: application/json" \
  -d '{"user": "dispatch@desertline.example", "password": "YOUR_PASSWORD"}'
```

```json
{
  "status": 200,
  "msg": "Logged in successfully",
  "msgs": [],
  "data": {
    "access_token": "<transporter-token>",
    "expiry_time": "2026-10-28 16:10:23",
    "transporter": { "id": 7019257, "name": "Desert Line Transport" },
    "user": { "id": 7019257, "name": "Desert Line Transport", "is_owner": true }
  }
}
```

* The token is in `data.access_token`. It lasts 90 minutes and cannot be refreshed. Log in again when it expires.
* Each login returns a new token. Earlier tokens keep working until they expire.
* Changing the password ends every token for that login.
* A team member (`is_owner: false`) acts for the whole transporter: same offers, trips and drivers.

## Send the token

Send the token in the `Authorization` header:

```bash
curl "https://lastmile.milenow.com/index.php/api/v1/partners/shipment-trips" \
  -H "Authorization: Bearer $MILE_TOKEN"
```

You can also send it as an `access_token` query parameter, or as an `access_token` field in a JSON body. The examples in these docs use `$MILE_TOKEN` for the company token and `$TRANSPORTER_TOKEN` for the transporter token.

> **Warning**
>
> The company token works only on company endpoints. The transporter token works only on `/api/v1/partners/transporters/...`. The wrong token returns 401 `invalid access`.

## Rate limits

Mile sets a limit of requests per minute for your company. Each login has its own counter. The minute starts with your first request and resets 60 seconds later.

FTL trip and transporter endpoints return these headers once the token is accepted:

| Header                  | Meaning                         |
| ----------------------- | ------------------------------- |
| `X-RateLimit-Limit`     | Requests allowed per minute     |
| `X-RateLimit-Remaining` | Requests left in this minute    |
| `X-RateLimit-Reset`     | Seconds until the minute resets |

When you go over the limit you get 429 with a `Retry-After` header in seconds. Wait that long, then retry.

```json
{
  "status": 429,
  "msg": "Rate limit reached: 200 requests per minute. Try again in 42 seconds.",
  "msgs": ["Rate limit reached: 200 requests per minute. Try again in 42 seconds."],
  "data": []
}
```

## Response envelope

Every response has the same four fields. The HTTP status code and `status` are always equal.

| Field    | On success    | On error                                    |
| -------- | ------------- | ------------------------------------------- |
| `status` | `200`         | The error code                              |
| `msg`    | What happened | The problem, or how many problems there are |
| `msgs`   | `[]`          | Every problem, one per item                 |
| `data`   | The result    | `[]`                                        |

## Status codes

| Code | Meaning                                                                                  | What to do                                            |
| ---- | ---------------------------------------------------------------------------------------- | ----------------------------------------------------- |
| 200  | Done. Trip changes can still carry `data.warnings`.                                      | Read `data`, and check `warnings`                     |
| 400  | `Invalid JSON body`                                                                      | Send valid JSON with `Content-Type: application/json` |
| 401  | `invalid access`, `Your token expired.`, or a bad login                                  | Log in again, or use the right token for the endpoint |
| 403  | `Your partner account is disabled` on a transporter endpoint                             | Ask the company to enable the transporter account     |
| 404  | The trip or stop is not in your account, or not outsourced or offered to the transporter | Check the id                                          |
| 409  | The request is valid but conflicts with the current state                                | Read `msg`, change the state, then retry              |
| 412  | Validation failed                                                                        | Fix every item in `msgs`, then retry                  |
| 429  | Rate limit reached                                                                       | Wait `Retry-After` seconds                            |
| 500  | `Something went wrong, please try again`                                                 | Retry later. Contact Mile if it repeats               |

### Validation errors (412)

Mile checks the whole request and returns **every** problem at once in `msgs`. Each item says where the problem is, for example `stops[1].shipment: containers[0].weight is required (0 or more)`. Fix them all before you retry.

```json
{
  "status": 412,
  "msg": "3 problems found, listed in msgs. Fix all of them and send again.",
  "msgs": [
    "stops[0]: stop has unknown fields shipmentId, pickupAt. Valid fields: leg, shipment_id, shipment, pickup_at, pickup_to, deliver_from, deliver_to",
    "stops[0]: send shipment_id or a shipment object",
    "stops[1].shipment: customer.id not found. It is a customer id (consigner.id or consignee.id in shipment responses), not merchant.id"
  ],
  "data": []
}
```

Unknown fields are rejected, and the message lists the valid ones. Field names are `snake_case`.

### Conflicts (409)

A 409 means the request is valid but cannot be done in the trip's current state. Validation runs first, so you only see a 409 once the request passes validation. Common conflicts:

* An FTL is already on another trip.
* An FTL with the same `shipment_no`, `bl_no` and container already exists. The message gives its `shipment_id`.
* The trip is outsourced, so you cannot add stops or assign your driver.
* The trip has started.
* The driver is busy on another trip at the same time.
* The stop has started, or it is the last stop on the trip.
* No offer in the request could be accepted or rejected.

The [business rules](/ftl-trips/business-rules) explain each one.

### Warnings

Requests that change a trip return `data.warnings`. A warning means the change was saved but a follow-up step failed, for example `ETAs could not be calculated: ...`. [Optimize the trip](/ftl-trips/guides/manage-stops#optimize-a-trip-web) in the Mile web app to retry.

## Formats

* Request dates: `Y-m-d`, for example `2026-11-03`.
* Request times: `H:i` in 24-hour time, for example `09:00`.
* Response date-times: `Y-m-d H:i:s` in your company's time zone, with no offset.
* Coordinates can come back as numbers or numeric strings. Accept both.
* `order_id` can be larger than a 32-bit integer. Store ids as 64-bit integers.