{
  "openapi": "3.1.0",
  "info": {
    "title": "FS Logistik Partner API",
    "version": "1.0.0",
    "summary": "Create shipments in the FS Logistik order system and follow their status.",
    "description": "Partner API for companies that hand shipments to FS Logistik. One account, one token. Rates and prices are never part of this API.",
    "contact": {
      "name": "FS Logistik",
      "url": "https://docs.fslogistik.com"
    }
  },
  "servers": [
    {
      "url": "https://admin.fslogistik.com/api/v1",
      "description": "Production"
    },
    {
      "url": "https://test-admin.sendpakke.nu/api/v1",
      "description": "Sandbox"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "Authentication",
      "description": "Exchange account credentials for a long-lived API token."
    },
    {
      "name": "Shipments",
      "description": "Create shipments and follow their status."
    }
  ],
  "x-guide": [
    {
      "id": "overview",
      "title": "Overview",
      "blocks": [
        {
          "type": "p",
          "text": "This API lets you register shipments directly in the FS Logistik order system and check where each one stands. Every shipment is bound to your account, so it appears in the FS Logistik panel alongside all other work under that account."
        },
        {
          "type": "p",
          "text": "The API covers two jobs and deliberately nothing else: creating a shipment, and tracking one. Rates and prices are not part of it. Pricing is calculated on the FS Logistik side from the account's price list and never returned in any response."
        },
        {
          "type": "note",
          "tone": "info",
          "label": "Conventions",
          "text": "All requests and responses are JSON. Send Content-Type: application/json and Accept: application/json. Timestamps are ISO 8601 with an offset, in Europe/Copenhagen time."
        }
      ]
    },
    {
      "id": "start",
      "title": "Getting started",
      "blocks": [
        {
          "type": "steps",
          "items": [
            "FS Logistik opens a partner account for your company in its admin panel and assigns it a price list and payment terms.",
            "FS Logistik sends you the account's email address and password over a secure channel.",
            "You call POST /token once with those credentials and store the API token it returns. The password is never sent again.",
            "You integrate against the sandbox server and create a few test shipments end to end.",
            "FS Logistik reviews the sandbox shipments in the panel, then opens your production account."
          ]
        }
      ]
    },
    {
      "id": "sandbox",
      "title": "Sandbox",
      "blocks": [
        {
          "type": "p",
          "text": "The sandbox runs the same code as production against a separate database. Shipments created there are visible to FS Logistik staff in a test panel, never dispatched, and never invoiced. Use it for the whole integration."
        },
        {
          "type": "table",
          "head": [
            "Setting",
            "Value"
          ],
          "rows": [
            [
              "Server",
              "`https://test-admin.sendpakke.nu/api/v1`"
            ],
            [
              "Email",
              "`sandbox@fslogistik.com`"
            ],
            [
              "Password",
              "`Sandbox-2026!`"
            ]
          ]
        },
        {
          "type": "p",
          "text": "A good first run: create a shipment with your own reference, read it back, then send the same create request again and confirm it returns 200 with the same order number instead of a second shipment."
        },
        {
          "type": "note",
          "tone": "warn",
          "label": "Shared account",
          "text": "This account is shared by everyone integrating against the sandbox, so treat everything in it as public: use made-up names and addresses, never real customer data. Prefix your references with something of your own (your company initials, say) so your shipments are easy to tell apart. The sandbox is cleared periodically."
        }
      ]
    },
    {
      "id": "postman",
      "title": "Postman collection",
      "blocks": [
        {
          "type": "p",
          "text": "Every endpoint on this page, with example requests and responses, ready to run."
        },
        {
          "type": "actions",
          "items": [
            {
              "label": "Download the Postman collection",
              "href": "postman/fs-logistik-partner-api.postman_collection.json",
              "primary": true
            },
            {
              "label": "Download openapi.json",
              "href": "openapi.json"
            }
          ]
        },
        {
          "type": "p",
          "text": "Import the collection with File → Import in Postman. It comes preloaded with the shared sandbox account: run **Get an API token** once and the token is stored automatically for the other requests; change `reference` to something of your own before creating a shipment."
        },
        {
          "type": "note",
          "tone": "info",
          "label": "Kept in step",
          "text": "The collection is generated from the same specification as this page, so the two cannot drift apart. If you prefer, import `openapi.json` directly into Postman or Insomnia instead of the collection."
        }
      ]
    },
    {
      "id": "statuses",
      "title": "Statuses",
      "blocks": [
        {
          "type": "p",
          "text": "A shipment always sits in exactly one of these. They are the same values FS Logistik staff see in the panel."
        },
        {
          "type": "table",
          "head": [
            "Value",
            "Meaning"
          ],
          "rows": [
            [
              "`pending`",
              "Registered, not yet picked up for processing."
            ],
            [
              "`processing`",
              "Being prepared — labelling, customs paperwork, carrier booking."
            ],
            [
              "`shipped`",
              "Handed to the carrier. A tracking number is normally present from here on."
            ],
            [
              "`delivered`",
              "Delivered to the receiver. Terminal."
            ],
            [
              "`cancelled`",
              "Cancelled before dispatch. Terminal."
            ]
          ]
        },
        {
          "type": "p",
          "text": "Once a shipment is with a carrier, FS Logistik polls that carrier hourly and writes the result back automatically. carrier_status is the carrier's own latest movement text and carrier_status_at is when that movement happened, so they may use wording this page does not define. Every status change is appended to history with the time the movement actually occurred, not the time it was recorded."
        },
        {
          "type": "note",
          "tone": "info",
          "label": "Polling",
          "text": "Checking more often than every 30 minutes will not surface anything new. New status values may be added over time — treat an unrecognised value as \"in progress\" rather than failing."
        }
      ]
    },
    {
      "id": "errors",
      "title": "Errors",
      "blocks": [
        {
          "type": "p",
          "text": "Errors use standard HTTP status codes with a consistent body. Field-level problems are listed under errors, keyed by the field path from the request."
        },
        {
          "type": "code",
          "lang": "json",
          "text": "{\n  \"success\": false,\n  \"message\": \"Validation failed\",\n  \"errors\": {\n    \"packages.0.weight_kg\": [\"The weight must not be greater than 30.\"],\n    \"receiver.address.postal_code\": [\"The receiver postal code is required.\"]\n  }\n}"
        },
        {
          "type": "table",
          "head": [
            "Code",
            "Meaning",
            "What to do"
          ],
          "rows": [
            [
              "`401`",
              "Token missing, malformed, or revoked",
              "Check the Authorization header. Do not retry."
            ],
            [
              "`403`",
              "Account not allowed to use the partner API",
              "Contact FS Logistik."
            ],
            [
              "`404`",
              "No shipment with that reference on this account",
              "Check the reference. Do not retry."
            ],
            [
              "`422`",
              "Validation failed",
              "Fix the fields listed in `errors` and send again."
            ],
            [
              "`429`",
              "Rate limit exceeded",
              "Wait for the number of seconds in `Retry-After`."
            ],
            [
              "`500`",
              "Something broke on the FS Logistik side",
              "Retry with the same reference — it will not duplicate."
            ]
          ]
        }
      ]
    },
    {
      "id": "limits",
      "title": "Limits",
      "blocks": [
        {
          "type": "table",
          "head": [
            "Limit",
            "Value"
          ],
          "rows": [
            [
              "Weight per package",
              "0.1 – 30 kg (documents: 0.1 – 2 kg)"
            ],
            [
              "Dimensions",
              "Length ≤ 120 cm, width ≤ 50 cm, height ≤ 50 cm"
            ],
            [
              "Quantity per package line",
              "1 – 100"
            ],
            [
              "Package lines per shipment",
              "1 – 50"
            ],
            [
              "Request rate",
              "60 requests per minute per token, 10 per minute on `/token`"
            ],
            [
              "Request body",
              "2 MB"
            ]
          ]
        },
        {
          "type": "p",
          "text": "These mirror the limits already enforced in the FS Logistik order system. If your shipments regularly fall outside them, say so before integration — the limits can be raised per account, but only deliberately."
        }
      ]
    },
    {
      "id": "open",
      "title": "Open points",
      "blocks": [
        {
          "type": "p",
          "text": "Three things are agreed with FS Logistik before a production account is opened. None of them change the shape of the API."
        },
        {
          "type": "list",
          "items": [
            "**Payment terms.** Whether the account is prepaid or invoiced, and on what cycle.",
            "**Weight and dimension limits.** Whether the standard limits above cover your traffic.",
            "**Cancellations.** Whether you need to cancel a shipment through the API, or whether cancellation stays a manual request to FS Logistik."
          ]
        }
      ]
    }
  ],
  "paths": {
    "/token": {
      "post": {
        "tags": [
          "Authentication"
        ],
        "operationId": "createToken",
        "summary": "Get an API token",
        "security": [],
        "description": "Exchanges the account's panel credentials for a long-lived API token. Run this once and store the token; the password is never sent again. Wrong credentials return 401. After five failed attempts in a minute the endpoint returns 429.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TokenRequest"
              },
              "example": {
                "email": "api@yourcompany.example",
                "password": "the account password",
                "name": "Partner integration"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Token created",
            "content": {
              "application/json": {
                "example": {
                  "token": "sk_live_9f2c41a7e8b34d05a6c1e7fbd9204e83",
                  "token_type": "Bearer",
                  "account": "Your Company A/S",
                  "created_at": "2026-08-27T14:02:11+02:00"
                }
              }
            }
          },
          "401": {
            "description": "Invalid credentials",
            "content": {
              "application/json": {
                "example": {
                  "success": false,
                  "message": "Invalid credentials."
                }
              }
            }
          },
          "403": {
            "description": "Account cannot use the partner API",
            "content": {
              "application/json": {
                "example": {
                  "success": false,
                  "message": "This account cannot be used with the partner API."
                }
              }
            }
          }
        }
      }
    },
    "/shipments": {
      "post": {
        "tags": [
          "Shipments"
        ],
        "operationId": "createShipment",
        "summary": "Create a shipment",
        "description": "Registers a shipment on your account and returns the FS Logistik order number. No pricing fields are accepted; any that are sent are ignored. The reference is the idempotency key: sending the same reference twice returns the existing shipment with 200 instead of creating a duplicate, so retrying after a timeout is safe.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ShipmentRequest"
              },
              "example": {
                "reference": "REF-889271",
                "order_type": "package",
                "content_description": "Textile samples",
                "sender": {
                  "name": "Atlas Tekstil A.S.",
                  "phone": "+90 212 555 0134",
                  "email": "export@atlastekstil.com",
                  "address": {
                    "street": "Mahmutbey Mah. Tasocagi Cad. No 12",
                    "city": "Istanbul",
                    "postal_code": "34218",
                    "country_code": "TR"
                  }
                },
                "receiver": {
                  "name": "Lise Andersen",
                  "phone": "+45 26 13 42 16",
                  "email": "lise.andersen@example.dk",
                  "address": {
                    "street": "Nyholms Alle 30 B",
                    "city": "Rodovre",
                    "postal_code": "2610",
                    "country_code": "DK"
                  }
                },
                "packages": [
                  {
                    "quantity": 1,
                    "weight_kg": 2.4,
                    "length_cm": 40,
                    "width_cm": 30,
                    "height_cm": 20
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Shipment created",
            "content": {
              "application/json": {
                "example": {
                  "reference": "REF-889271",
                  "order_number": "20260827004",
                  "status": "pending",
                  "created_at": "2026-08-27T14:12:08+02:00"
                }
              }
            },
            "x-primary": true
          },
          "200": {
            "description": "Reference already used — the existing shipment is returned",
            "content": {
              "application/json": {
                "example": {
                  "reference": "REF-889271",
                  "order_number": "20260827004",
                  "status": "pending",
                  "created_at": "2026-08-27T14:12:08+02:00"
                }
              }
            }
          },
          "422": {
            "description": "Validation failed",
            "content": {
              "application/json": {
                "example": {
                  "success": false,
                  "message": "Validation failed",
                  "errors": {
                    "packages.0.weight_kg": [
                      "The weight must not be greater than 30."
                    ]
                  }
                }
              }
            }
          }
        }
      }
    },
    "/shipments/track/{reference}": {
      "get": {
        "tags": [
          "Shipments"
        ],
        "operationId": "getShipment",
        "summary": "Track a shipment",
        "description": "Returns the current status of one shipment and how it got there. Only shipments created by your own token are visible; anything else returns 404. carrier and tracking_number are null until the shipment is handed to a carrier. The response carries no prices, no invoice data, and no billing details.",
        "parameters": [
          {
            "name": "reference",
            "in": "path",
            "required": true,
            "description": "Your own reference for the shipment.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Shipment status",
            "content": {
              "application/json": {
                "example": {
                  "reference": "REF-889271",
                  "order_number": "20260827004",
                  "status": "shipped",
                  "status_label": "Shipped",
                  "carrier": "ups",
                  "tracking_number": "1ZK919J46899000001",
                  "carrier_status": "In Transit to Destination",
                  "carrier_status_at": "2026-08-29T09:41:55+02:00",
                  "updated_at": "2026-08-29T09:44:02+02:00",
                  "history": [
                    {
                      "status": "pending",
                      "occurred_at": "2026-08-27T14:12:08+02:00"
                    },
                    {
                      "status": "processing",
                      "occurred_at": "2026-08-28T08:30:11+02:00"
                    },
                    {
                      "status": "shipped",
                      "occurred_at": "2026-08-29T09:41:55+02:00"
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "Not found on this account",
            "content": {
              "application/json": {
                "example": {
                  "success": false,
                  "message": "No shipment with that reference on this account."
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "The token returned by POST /token."
      }
    },
    "schemas": {
      "TokenRequest": {
        "type": "object",
        "required": [
          "email",
          "password"
        ],
        "properties": {
          "email": {
            "type": "string",
            "format": "email",
            "description": "The account email FS Logistik gave you."
          },
          "password": {
            "type": "string",
            "description": "The account password."
          },
          "name": {
            "type": "string",
            "maxLength": 100,
            "description": "A label for this token, shown to FS Logistik staff."
          }
        }
      },
      "Address": {
        "type": "object",
        "required": [
          "street",
          "city",
          "postal_code",
          "country_code"
        ],
        "properties": {
          "street": {
            "type": "string",
            "maxLength": 500
          },
          "city": {
            "type": "string",
            "maxLength": 255
          },
          "postal_code": {
            "type": "string",
            "maxLength": 20
          },
          "country_code": {
            "type": "string",
            "minLength": 2,
            "maxLength": 2,
            "description": "ISO 3166-1 alpha-2, uppercase. The country name is resolved on the FS Logistik side."
          }
        }
      },
      "Party": {
        "type": "object",
        "required": [
          "name",
          "phone",
          "address"
        ],
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 255
          },
          "phone": {
            "type": "string",
            "maxLength": 50
          },
          "email": {
            "type": "string",
            "format": "email",
            "description": "Optional, but this is what triggers the receiver's notifications."
          },
          "address": {
            "$ref": "#/components/schemas/Address"
          }
        }
      },
      "Package": {
        "type": "object",
        "required": [
          "quantity",
          "weight_kg",
          "length_cm",
          "width_cm",
          "height_cm"
        ],
        "properties": {
          "quantity": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100
          },
          "weight_kg": {
            "type": "number",
            "minimum": 0.1,
            "maximum": 30
          },
          "length_cm": {
            "type": "number",
            "minimum": 1,
            "maximum": 120
          },
          "width_cm": {
            "type": "number",
            "minimum": 1,
            "maximum": 50
          },
          "height_cm": {
            "type": "number",
            "minimum": 1,
            "maximum": 50
          }
        }
      },
      "ShipmentRequest": {
        "type": "object",
        "required": [
          "reference",
          "order_type",
          "content_description",
          "sender",
          "receiver",
          "packages"
        ],
        "properties": {
          "reference": {
            "type": "string",
            "maxLength": 255,
            "description": "Your own identifier. Unique within your account; used for tracking and for idempotency."
          },
          "order_type": {
            "type": "string",
            "enum": [
              "package",
              "document"
            ]
          },
          "content_description": {
            "type": "string",
            "maxLength": 1000,
            "description": "Plain description of the goods, in English. Used on customs paperwork."
          },
          "sender": {
            "$ref": "#/components/schemas/Party"
          },
          "receiver": {
            "$ref": "#/components/schemas/Party"
          },
          "packages": {
            "type": "array",
            "minItems": 1,
            "maxItems": 50,
            "items": {
              "$ref": "#/components/schemas/Package"
            }
          }
        }
      }
    }
  },
  "x-links": [
    {
      "label": "Postman collection",
      "href": "postman/fs-logistik-partner-api.postman_collection.json"
    },
    {
      "label": "openapi.json",
      "href": "openapi.json"
    }
  ]
}
