# 🔽 Downstream API - send to 🌐

> The Downstream API: create, update and deactivate tradeflows in Dockflow.

> **Base URL:** `https://api.dockflow.com/v3` (or `https://uat.dockflow.com/v3` for the
> [sandbox](/api/uat))

---

## Endpoints

| Method     | Path                                           | Description                                                                            |
| ---------- | ---------------------------------------------- | -------------------------------------------------------------------------------------- |
| **POST**   | `/downstream/tradeflows`                       | Create a tradeflow or update an existing one. Accepts a single object **or** an array. |
| **DELETE** | `/downstream/tradeflows/{tradeflow_reference}` | Deactivate a tradeflow.`?hard=true` → permanently delete.                         |

---

## 1 · Create / update tradeflows `POST /downstream/tradeflows`

### Payload format

A single JSON object **or** an array of objects.

```jsonc
{
  "tradeflow_reference": "PO4564268", // required

  // Tracking references — string OR array
  "container_reference": [
    {
      "reference": "ABCD1234567",
      "custom_properties": {
        "requires_inspection": true,
        "quality_check_date": "2025-05-01"
      }
    },
    {
      "reference": "EFGH7654321",
      "custom_properties": {
        "requires_inspection": true,
        "quality_check_date": "2025-05-01"
      }
    }
  ],
  "container_references_complete": true,

  "bill_of_lading_reference": "ANR00001010",
  "booking_reference": ["94512540", "94512888"],

  // Tradeflow status
  "active": true, // optional

  // Carrier name
  "carrier_name": "Maersk", // optional
  "carrier_scac": "MAEU", // optional

  // Partner shortcuts
  "customer_name": "Fruit Maker EXIM International",
  "customer_reference": "PO_12345_CUST", // optional
  "partners": [
    {
      "name": "Best Forwarder BV",
      "role": "forwarder",
      "reference": "PO_4564268",
      "lead_contact": "contact@forwarder.com" // optional - email of lead contact at partner
    }
  ],
  "partners_complete": false,

  // Team assignment
  "team_name": "Sales Team EMEA", // optional

  // Lead contact - individual responsible for this tradeflow
  "lead_contact": "sophie@company.com", // optional - email or full name

  // Voyage metadata
  "port_of_loading": "BEANR",
  "port_of_discharge": "USLAX",

  "estimated_time_of_departure": "2025-05-14T12:00:00Z",
  // or - when vessel has already departed
  "actual_time_of_departure": "2025-05-14T12:00:00Z",

  "estimated_time_of_arrival": "2025-06-06T22:00:00Z",
  // or - when vessel has already arrived
  "actual_time_of_arrival": "2025-06-06T22:00:00Z",

  "vessel": "Maersk SOPHIE",
  "incoterms": "FOB",
  "references": {
    "house_bill_of_lading": "hbl_123456789", // optional house bill of lading reference
    "delivery_location": "Factory yard 12345", // optional, stored as a free-text reference
    "delivery_event_date": "2021-08-24T15:00:25+00:00" // optional, stored as a free-text reference
  },

  // Optional self-defined events
  "events": [
    {
      "message": "delivery at client",
      "container_reference": "ABCD1234567",
      "location_name": "BEANR", // 5-character UN/LOCODE; any other value leaves the event without a location
      "event_date": "2025-05-12T08:34:00Z",
      "actual": true // whether the event already happened – or is scheduled
    }
  ],

  // Per-tradeflow / per-container free-time overrides (PATCH, append-only)
  "free_time_overrides": [
    { "dwell_type": "equipment_free_time_in_port", "free_days": 14 },
    { "dwell_type": "equipment_free_time_out_of_port", "free_days": 7,
      "container_reference": "ABCD1234567" }
  ],

  // NEW · Arbitrary key-value pairs defined by you
  "custom_references": {
    "invoice_number": "INV-12345",
    "priority": 2,
    "quality_check_date": "2025-05-01",
    "requires_inspection": true
  }
}
```

#### `custom_references` — what & why

| Field               | Type   | Description                                                                                                                                                                                                                      |
| ------------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `custom_references` | object | A _free-form dictionary_ whose keys are fully user-defined. Each value should be a string, number, boolean, or ISO-8601 date. Use it for anything that doesn’t fit the prescribed schema (e.g. internal IDs, workflow flags, etc.). |

- Values are stored as text. Use flat values only; do not nest objects or arrays.
- Keys are case-sensitive. A key you have not used before is created for your organisation.
- Sending `null` or an empty string for a key removes that property from the tradeflow; the strings `null` and `#null` are ignored.

#### Complete-list flags

| Flag                                 | Applies to                   | Effect when `true`                 |
| ------------------------------------ | ---------------------------- | ---------------------------------- |
| `container_references_complete`      | `container_reference[]`      | Containers not listed are removed. |
| `bill_of_lading_references_complete` | `bill_of_lading_reference[]` | Same logic.                        |
| `booking_references_complete`        | `booking_reference[]`        | Same logic.                        |
| `partners_complete`                  | `partners[]`                 | Partners not listed are removed.   |

If a _\_complete_ flag is omitted, the list is _merged_ instead of _replaced_.

#### Partner input

- **`customer_name`** — quick way to add one partner with role `customer`.
- **`partners`** — full array that supports multiple roles and references.
  Both inputs may be combined.

#### Team assignment

- **`team_name`** — assigns an existing team to the tradeflow.
  - Must be an existing team (minimum 3 characters)
  - First searches your organization, then partner organizations involved in the tradeflow
  - Case-insensitive matching (e.g., "Sales Team EMEA" matches "sales team emea")
  - If no matching team is found in any involved organization, the field is silently ignored

#### Lead contact

A **lead contact** is the individual user accountable for a tradeflow. They appear in the "My Tradeflows" view and receive assignment notifications. See the [Lead Contact guide](/user-management/lead-contact/) for the full feature overview.

- **`lead_contact`** on the tradeflow root — the person responsible for the whole shipment on your side.
- **`lead_contact`** on a partner object — the person responsible at that partner organization.

**Accepted values** (in order of reliability):

| Format | Example | Notes |
|--------|---------|-------|
| Email | `sophie@company.com` | Recommended — unambiguous exact match |
| Full name | `Sophie Dupont` | Matched against `first_name + last_name`, word separators are flexible |

**Matching rules:**
- Must resolve to an **existing user** within your organization (for the root-level field) or the partner organization (for the partner-level field).
- If the user belongs to a different entity than the one being set, the lead contact is also propagated to that entity's tradeflow record.
- If no user matches, the field is silently ignored — the tradeflow is still created/updated without a lead contact.

#### Free time overrides

Per-tradeflow and per-container overrides for free-time monitoring (demurrage, detention, storage, electricity). Useful when a contract grants a one-off extension or grace period that should win over the global free-time rules.

| Field                 | Type          | Required | Description                                                                                                                              |
| --------------------- | ------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `dwell_type`          | string (enum) | yes      | One of the 5 modern dwell types - see list below.                                                                                        |
| `free_days`           | integer ≥ 0   | yes\*    | Number of free days for this override. Required unless `cancelled: true`. Zero is valid.                                                 |
| `container_reference` | string        | no       | ISO 6346 container reference. If present and valid, the override is scoped to that container. If absent, the override is tradeflow-wide. |
| `cancelled`           | boolean       | no       | When `true`, cancels the override at this scope. `free_days` is ignored.                                                                 |

**Accepted `dwell_type` values:**

- `equipment_free_time_in_port` - equipment free time at the port
- `equipment_free_time_out_of_port` - equipment free time after gate-out
- `equipment_free_time_combined` - combined pool (carriers like Evergreen, or Maersk on some lanes)
- `storage_free_time` - terminal storage above carrier demurrage
- `electricity_free_time` - reefer electricity free time

**Semantics:**

- **PATCH, append-only.** Listed scopes are upserted; unlisted scopes are untouched. Re-posting the same value is a no-op (idempotent). An empty array `[]` is also a no-op.
- **Scope precedence.** A per-container override wins over a tradeflow-wide override for that container; tradeflow-wide applies to all other containers. A cancellation at one scope does not affect the override at the other scope.
- **Cancellation.** Send `{ "dwell_type": "...", "cancelled": true }` (optionally with a `container_reference`) to clear the override at that exact scope.
- **Pre-creation.** A per-container override may be authored before the container is added to the tradeflow; it is picked up automatically once the container appears.
- **Effective downstream.** Once accepted, the override is reflected in the upstream API output, in graph-tp transport plans, and in the free-time-exceeded flag generation (typically within a few minutes due to the cron's cooldown).

Invalid entries (unknown `dwell_type` — the older names `destination_demurrage` and `destination_detention` are not accepted, non-numeric or negative `free_days`) are silently skipped. A malformed `container_reference` is ignored and the entry is treated as tradeflow-wide.

### Response `202 Accepted`

Processing is asynchronous. The response acknowledges each queued item:

```jsonc
{
  "status": "queued",
  "tradeflows": [
    {
      "tradeflow_reference": "PO4564268",
      "warnings": []
    }
  ]
}
```

`warnings` is reserved for soft-fail feedback and is currently always empty — a value that fails a
soft-fail rule is ignored silently. To see what Dockflow did with a payload, open the tradeflow in
the app and use **Download ▸ Download V3 API**, or read it back from
[the upstream API](/api/upstream). Hard-fail rules are reported synchronously as
`400 Bad Request`.

---

## 2 · Remove tradeflows `DELETE /downstream/tradeflows/{tradeflow_reference}`

| Action                     | How                                                                                        |
| -------------------------- | ------------------------------------------------------------------------------------------ |
| **Deactivate** (preferred) | `DELETE /downstream/tradeflows/PO4564268` — same effect as posting `"active": false`. |
| **Permanent delete**       | `DELETE /downstream/tradeflows/PO4564268?hard=true`                                        |

Returns `204 No Content` on success.

### Bulk delete

Using `*` as the `tradeflow_reference` deactivates **every** tradeflow of the organisation the token
belongs to. Add `?hard=true` for permanent deletion.

```
DELETE /downstream/tradeflows/*
```

| Environment             | Behaviour                                                                                                                   |
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| [UAT](/api/uat) | Allowed as-is.                                                                                                              |
| Production              | Requires `?confirm=PRODUCTION`. Without it the call is refused with `400 Bad Request`; every accepted production bulk delete is logged. When the organisation has no tradeflows, the call returns `404`. |

---

## Validation rules

The request is accepted with `202 Accepted` as long as the body is valid JSON and is an object or an
array of objects; otherwise it is refused with `400 Bad Request`. Everything else is processed after
the response, and an invalid value is skipped silently while the rest of the payload is applied.
Check the outcome in the app or via the [upstream API](/api/upstream).

`tradeflow_reference` is required. An item without a usable (non-blank) reference is not imported.

Values that are skipped silently:

- `container_reference` not matching `^[A-Z]{4}[0-9]{7}$` after upper-casing and removing spaces and hyphens.
- Empty strings or null values inside array fields.
- Dates that cannot be parsed (use ISO-8601, or `YYYYMMDD` for `estimated_time_of_arrival`).
- `port_of_loading` / `port_of_discharge` that are not a 2-letter ISO country code or a 5-character UN/LOCODE.
- `carrier_name` or `carrier_scac` shorter than 4 characters.
- `customer_name` shorter than 2 characters; partner `name` of 2 characters or fewer.
- `incoterms` of 2 characters or fewer.
- Bill of lading and booking references shorter than 5 characters.
- `team_name` shorter than 3 characters or not matching an existing team.
- `lead_contact` that matches no existing user.
- Unknown top-level properties (outside `custom_references`).
- On the [UAT](/api/uat) host, `customer_name` and `partners` are not processed.

When `active` is omitted, an inactive tradeflow is reactivated only if its arrival is in the future,
or, with no arrival date, if it was created within the last two months. A tradeflow closed because all
its containers were returned is the exception: without an arrival date, it is reactivated only when the
request names a container it does not have yet. `"active": false` deactivates it.

---

## Error codes

| Status                        | Meaning                                                                                  |
| ----------------------------- | ---------------------------------------------------------------------------------------- |
| **400 Bad Request**           | Body is not valid JSON, or not an object or array. Also `Token format invalid`.          |
| **401 Unauthorized**          | Missing or malformed `Authorization` header, or unknown token.                           |
| **403 Forbidden**             | Token environment does not match the host (`prod_` vs `uat_`), or the token is revoked.  |
| **404 Not Found**             | Tradeflow not found on delete.                                                           |
| **500 Internal Server Error** | Unexpected error during processing.                                                      |

---

Questions or feedback? Create an issue in the public repository or contact Dockflow support.
