# Shipping rules

Shipping rules turn your shipping policy into something Ship Jay enforces. A rule says: when these conditions hold, do these actions. Rules run on every quote, whether it comes from the app, a spreadsheet, the API or an AI agent. A rate a rule removes is removed from the quote itself, so it can never be bought.

The API manages the same rules you see in Settings, Shipping rules. Reading needs `rules:read`. Creating, replacing and deleting need `rules:write`.

| Call | What it does |
| --- | --- |
| `GET /rules` | List your rules. |
| `POST /rules` | Create a rule. Returns `201` and `{ "id": "..." }`. |
| `PUT /rules/{id}` | Replace a rule. Send the full rule. |
| `DELETE /rules/{id}` | Delete a rule. |

## Anatomy of a rule

```json
{
  "name": "Sign for high value parcels",
  "phase": "shipment",
  "priority": 100,
  "enabled": true,
  "stop_on_match": false,
  "conditions": { "all": [ { "field": "declared_value_cents", "op": "gte", "value": 20000 } ] },
  "actions": [ { "type": "require_signature" }, { "type": "set_insurance", "amount_cents": 20000 } ]
}
```

| Field | Meaning |
| --- | --- |
| `name` | 1 to 80 characters. |
| `phase` | `shipment` runs before rating. `rate` runs on the rates that came back. |
| `priority` | 1 to 10000, default 100. Lower numbers run first. |
| `enabled` | Default `true`. |
| `stop_on_match` | When `true`, a rule that matches ends its phase. Later rules in that phase are skipped. |
| `conditions` | A group, `{ "all": [...] }` or `{ "any": [...] }`. Groups can nest. Default is `{ "all": [] }`, which always matches. An empty `any` never matches. |
| `actions` | 1 to 10 actions. Which ones are allowed depends on the phase. |

## Fields

A condition compares a field to a value. Text comparisons ignore case and surrounding spaces.

| Field | Type | Phases |
| --- | --- | --- |
| `to.country` | text, two letter code | shipment, rate |
| `to.state` | text | shipment, rate |
| `to.zip` | text | shipment, rate |
| `international` | true or false, true when the country is not US | shipment, rate |
| `weight_oz` | number | shipment, rate |
| `packaging_type` | text | shipment, rate |
| `declared_value_cents` | number, the larger of the insurance amount and the customs value | shipment, rate |
| `reference1` | text | shipment, rate |
| `rate.carrier` | text | rate only |
| `rate.service` | text | rate only |
| `rate.price_cents` | number | rate only |
| `rate.days` | number, estimated delivery days | rate only |

A shipment rule that uses a `rate.` field is rejected with `422`.

## Operators

| Operator | Meaning |
| --- | --- |
| `eq`, `neq` | equals, does not equal |
| `gt`, `gte`, `lt`, `lte` | numeric comparisons |
| `in`, `not_in` | the value is in, or not in, a list. Send `value` as an array of up to 50 entries. |
| `starts_with`, `contains` | text comparisons |

If a rate has no estimated delivery time, `rate.days` has no value and the condition only holds for `neq` and `not_in`.

## Actions

**Shipment phase**

| Action | Effect |
| --- | --- |
| `{ "type": "block", "message": "..." }` | The quote fails with `422` and your message. `details.blockedByRule` is `true`. |
| `{ "type": "require_signature" }` | Turns on signature on delivery. |
| `{ "type": "set_insurance", "amount_cents": 20000 }` | Insures the parcel. Between 100 and 500000 cents. The highest amount among matching rules wins. |

**Rate phase**

| Action | Effect |
| --- | --- |
| `{ "type": "exclude_carrier", "carrier": "UPS" }` | Removes rates from that carrier. |
| `{ "type": "exclude_service", "contains": "Express" }` | Removes rates whose service name contains the text. |
| `{ "type": "max_price", "cents": 3000 }` | Removes rates above that price. |
| `{ "type": "max_days", "days": 5 }` | Removes rates estimated to take longer. Rates with no estimate stay. |
| `{ "type": "choose_rate", "strategy": "cheapest" }` | Picks a rate and returns it as `suggested_rate_id`. Strategies: `cheapest`, `fastest`, `best` (a balance of price and speed). Add `"carrier"` to pick only from one carrier. |

In rate rules, the conditions are tested against each rate. `choose_rate` chooses among the rates that match the conditions and survived earlier removals. The first rule to choose wins, and the chosen rate gets the `rule` badge. A rule that chooses a rate does not buy it. Your code still has to call buy.

## Worked example

Goal: sign for and insure expensive parcels, never use UPS for parcels over 50 lb, and suggest the cheapest option that arrives within five days.

```bash
curl https://shipjay.com/api/v1/rules \
  -H "Authorization: Bearer $SHIPJAY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Sign for high value parcels",
    "phase": "shipment",
    "priority": 10,
    "conditions": { "all": [ { "field": "declared_value_cents", "op": "gte", "value": 20000 } ] },
    "actions": [ { "type": "require_signature" }, { "type": "set_insurance", "amount_cents": 20000 } ]
  }'

curl https://shipjay.com/api/v1/rules \
  -H "Authorization: Bearer $SHIPJAY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "No UPS over 50 lb",
    "phase": "rate",
    "priority": 10,
    "conditions": { "all": [ { "field": "weight_oz", "op": "gt", "value": 800 } ] },
    "actions": [ { "type": "exclude_carrier", "carrier": "UPS" } ]
  }'

curl https://shipjay.com/api/v1/rules \
  -H "Authorization: Bearer $SHIPJAY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Cheapest within five days",
    "phase": "rate",
    "priority": 50,
    "conditions": { "all": [ { "field": "rate.days", "op": "lte", "value": 5 } ] },
    "actions": [ { "type": "choose_rate", "strategy": "cheapest" } ]
  }'
```
```js
const rules = [
  {
    name: "Sign for high value parcels",
    phase: "shipment",
    priority: 10,
    conditions: { all: [{ field: "declared_value_cents", op: "gte", value: 20000 }] },
    actions: [{ type: "require_signature" }, { type: "set_insurance", amount_cents: 20000 }],
  },
  {
    name: "No UPS over 50 lb",
    phase: "rate",
    priority: 10,
    conditions: { all: [{ field: "weight_oz", op: "gt", value: 800 }] },
    actions: [{ type: "exclude_carrier", carrier: "UPS" }],
  },
  {
    name: "Cheapest within five days",
    phase: "rate",
    priority: 50,
    conditions: { all: [{ field: "rate.days", op: "lte", value: 5 }] },
    actions: [{ type: "choose_rate", strategy: "cheapest" }],
  },
];

for (const rule of rules) {
  const res = await fetch("https://shipjay.com/api/v1/rules", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.SHIPJAY_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify(rule),
  });
  console.log(res.status, await res.json());
}
```
```python
rules = [
    {
        "name": "Sign for high value parcels",
        "phase": "shipment",
        "priority": 10,
        "conditions": {"all": [{"field": "declared_value_cents", "op": "gte", "value": 20000}]},
        "actions": [{"type": "require_signature"}, {"type": "set_insurance", "amount_cents": 20000}],
    },
    {
        "name": "No UPS over 50 lb",
        "phase": "rate",
        "priority": 10,
        "conditions": {"all": [{"field": "weight_oz", "op": "gt", "value": 800}]},
        "actions": [{"type": "exclude_carrier", "carrier": "UPS"}],
    },
    {
        "name": "Cheapest within five days",
        "phase": "rate",
        "priority": 50,
        "conditions": {"all": [{"field": "rate.days", "op": "lte", "value": 5}]},
        "actions": [{"type": "choose_rate", "strategy": "cheapest"}],
    },
]

for rule in rules:
    res = requests.post(
        "https://shipjay.com/api/v1/rules",
        headers={"Authorization": f"Bearer {os.environ['SHIPJAY_KEY']}"},
        json=rule,
        timeout=30,
    )
    print(res.status_code, res.json())
```
Quote a 60 lb parcel after this and the UPS rates are gone. `applied_rules` in the quote response lists each rule that did something, with its `effects`, and `suggested_by_rule` names the rule that chose a rate.

**Note:** Rules change what every quote on the account returns, including quotes made by people in the app. Create them with care, and prefer a key that can only read rules for systems that do not need to change them.