FortoBeta
Start free

Warehouse API

For warehouse systems and the people who build on them. A store's warehouse calls Forto as it calls a carrier: for each order Forto ships, the warehouse system sends the order and its boxes, and Forto answers each box's label and tracking. Forto buys the labels on its own carrier accounts and handles the customs and duties from checkout. The warehouse sends no customs data and pays nothing for the labels.

Which orders are Forto’s

Forto marks every order it ships when the order is created. Route an order to Forto when it carries the Shopify tag forto; ship every other order as you do today. An order sold with duties paid also carries forto-duty-paid, and its shipping line’s code starts with forto_. You don’t act on either: Forto prepays the duties on those parcels.

The key

The store creates a key for its warehouse in Forto, under Settings, Warehouse, and gives it to you with the API’s address. Send it on every call as Authorization: Bearer fk_wh_…. A key buys labels only for that store’s orders. The store can disconnect it at any time; a disconnected key answers 401. A 3PL serving several stores holds one key per store.

Test before going live

Test against a Shopify development store, as you would any Shopify app: it takes test orders, and nothing on it costs money. The address and the calls are the same as in production.

  • ·Create a development store in your Shopify Partner account and install Forto on it. Choose a plan from Growth: a development store is never charged for one.
  • ·Place orders with Shopify’s test payments, choosing a Forto rate at checkout. They carry the forto tag, as real orders do.
  • ·In Forto, under Settings, Warehouse, create a key. On a development store it starts fk_wh_test_.
  • ·Every answer to a test key says "test": true. Its labels are test labels: no carrier is booked, nothing is charged, and they don’t ship. Voiding works the same.

A test key stays a test key. Once the store takes real orders it is refused with 409, and the store creates a live key, which starts fk_wh_.

Ask for the labels

One call per order, with all of its boxes, when the order is packed. A label usually comes back within a few seconds; allow 20 seconds before you give up and ask again.

POST {API URL}/labels
Authorization: Bearer fk_wh_…
Content-Type: application/json

{
  "order": "#1043",
  "parcels": [
    { "reference": "1", "weightKg": 0.8, "lengthCm": 30, "widthCm": 20, "heightCm": 10 }
  ],
  "shipFrom": {
    "name": "Køge Fulfilment", "address1": "Lyngvej 4", "city": "Køge",
    "zip": "4600", "country": "DK", "phone": "+45 56 00 00 00"
  }
}
FieldWhat to send
orderShopify’s order id (for example 5843121309) or the order’s name (#1043).
parcelsThe boxes, 1 to 20. One box ships the whole order.
parcels[].referenceYour own name for the box (its number, its SSCC). Optional: without it, a box is known by its place in the list.
parcels[].weightKgThe packed weight in kilograms. Required.
parcels[].lengthCm, widthCm, heightCmThe box in centimetres: all three, or none for the store’s default package.
parcels[].itemsWhat is in the box: Shopify’s variantId or the sku, with the quantity. Required when the order goes in more than one box, so each box declares only its own contents.
shipFromThe address the parcel leaves from: name, company, address1, address2, city, state, zip, country (two letters), phone, email.
shipToCountryOptional: the country you are shipping to. If it differs from the order’s, Forto refuses, because the duties were worked out for the order’s country.

The answer

201 Created   (200 when every box was answered before)

{
  "success": true,
  "data": {
    "orderId": "5843121309",
    "dutyPaid": true,
    "test": false,
    "parcels": [{
      "reference": "1",
      "trackingNumber": "1Z…",
      "trackingUrl": "https://www.ups.com/track?tracknum=1Z…",
      "carrier": "ups",
      "service": "UPS Standard",
      "labelPdf": "JVBERi0…",
      "labelUrl": "…/files/…/label.pdf",
      "invoicePdf": null,
      "invoiceUrl": null,
      "reused": false
    }]
  }
}
  • ·Print labelPdf (the PDF, base64) or fetch labelUrl. The link needs no key and opens for 30 days while the label ships.
  • ·An invoice comes back only where the carrier needs paper with the parcel. Print it and put it with the box; otherwise the invoice travels electronically.
  • ·Mark the order fulfilled in Shopify with trackingNumber and trackingUrl, as your system does for any carrier. Forto doesn’t.
  • ·test is true only on a development store: the labels are test labels and must not be shipped.

Safe to ask again

A box is answered once. Ask again for the same order and the same box references (a reprint, a retry after a timeout) and you get the same labels back with reused: true; nothing more is bought. If the store bought a label for the order itself, it is answered to the first box. If a call stops part way, the boxes already answered come back in data.parcels beside the error, and asking again buys only the rest.

Repack or cancel a box

DELETE {API URL}/labels/{trackingNumber}
Authorization: Bearer fk_wh_…

Voids a label this key was answered, and gives the store its charges back. Its link stops opening. To repack, void the box, then ask again with its reference and the new weight: a new label is bought.

At the end of the day

There is no end-of-day call. The carrier receives each parcel’s data when its label is bought, so there is nothing to close or upload before the parcels are collected.

When there is no label

{ "success": false, "code": "items", "error": "More of NK-1 is packed than order #1043 holds." }
StatusMeans
400The request is wrong: a box without a weight, several boxes without their items, an item not on the order, an incomplete ship-from address. Correct it and ask again.
401The key is unknown or disconnected.
403The store’s plan does not include the warehouse connection.
404Forto found no such order in the store’s Shopify.
409Forto does not ship this order: it is fulfilled, cancelled, or on a lane the store ships with its own carrier. Or the store must act first, for example on its business check. Or a test key is used on a store that now takes real orders. The error says which.
422The carrier would refuse the label as it stands, for example over an address. The error names what is missing.
503No label just now, for example a carrier not answering. Ask again in a few minutes.

Every error is written for the person at the packing table. Show it to them as it is.

← FAQ & troubleshooting