Fulfillment orders V2

When a physical product in a V2 subscription contract has been paid for, a fulfillment order is created: one order for each successful billing run. A monthly subscription to a bag of coffee therefore produces one open order every month, which has to be picked, packed and shipped.

A warehouse or fulfillment system receives the orders as they happen through fulfillment_order.* events. The read endpoints described here complement those events and are meant to:

  1. Fetch past orders when an integration is first put into use.

  2. Reconcile regularly and find any orders that are missing.

  3. Recover an order that was missed because an event failed to be delivered.

In addition, two write endpoints close the loop from the other side: a seller who ships the order themselves records the tracking number and fulfills the order (fulfill), or cancels it (cancel). Booking shipments and printing shipping labels are still not part of the API — that is done either in the dashboard or in the seller’s own system.

Fulfillment in an external system

Under Settings › Shipping you choose where orders are fulfilled:

Setting

Description

Disabled

No fulfillment orders are created.

In Áskell

Áskell books the shipment with the carrier, fetches the shipping label, and the order is fulfilled in the dashboard.

In an external system

Orders are created and sent out as before, but the seller’s warehouse system books the shipment itself and reports it to Áskell with fulfill.

With an external system, the booking buttons disappear from the dashboard, since Áskell has no dealings with the carrier; the order can still be fulfilled or canceled there.

The write endpoints are not tied to that setting, though. An account that fulfills in Áskell can use them just as well, for example to fulfill orders automatically from its own system after a shipment has been booked here.

See also

The events themselves are documented under Webhooks, and the contracts and billing runs that the orders are created from under Subscription Contracts V2.

Prerequisites

Three things are required for the endpoints to be available:

  1. A secret API key:

    Authorization: Api-Key your-secret-api-key
    
  2. The account must use V2 subscription contracts.

  3. Shipping (shipping) must be enabled for the account.

If any of these is missing, the endpoints return 403 with an explanation in detail.

The write endpoints (fulfill and cancel) additionally require fulfillment to be enabled on the account, whether it takes place in Áskell or in an external system. The read endpoints keep returning orders even when fulfillment is turned off — orders that already exist must not disappear — but none of them can then be changed here.

Note

A seller who sells physical products but has no carrier set up still receives orders. They then carry "shipping_selection": null and an empty fulfillments list; the order still has to be picked and fulfilled.

Endpoints

GET  /api/v2/fulfillment-orders/
GET  /api/v2/fulfillment-orders/{id}/
POST /api/v2/fulfillment-orders/{id}/fulfill/
POST /api/v2/fulfillment-orders/{id}/cancel/

{id} is the order’s id, not its number. number is the order’s sequential number within the account and works well for referring to the order with the customer, but it is not a lookup key in the API.

An order that belongs to another account returns 404, not 403.

By default, all orders are returned in a single list, newest first. If page_size is included, the response is paginated with count, next, previous and results.

Order statuses

Status

Description

open

The order is new and no shipment has been fulfilled yet.

partially_fulfilled

Part of the order has been fulfilled.

fulfilled

The order counts as fulfilled.

cancelled

The order was canceled.

Filters

Parameter

Description

status

One of the statuses above.

contract

The subscription contract ID.

customer

The customer ID.

customer_reference

The customer reference from your system.

updated_since

Orders that changed at or after the given time.

created_since

Orders created at or after the given time.

page_size

Number of orders per page. If omitted, everything is returned in a single list.

updated_since and created_since take an ISO 8601 timestamp, for example 2026-05-20T00:00:00Z. A bare date (2026-05-20) is interpreted as the start of that day.

Warning

An invalid filter returns an empty list, not an error. This applies both to a non-numeric contract or customer and to a timestamp that cannot be parsed. A reconciliation run therefore never gets every order back by accident when its cursor is malformed — but it should check that the filter is well-formed when it unexpectedly gets nothing back.

Reconciliation

The simplest reconciliation is based on updated_since: store the highest updated_at you have processed and use it as the cursor for the next run.

  • python
import requests

API_KEY = 'your api key here'
headers = {"Authorization": f"Api-Key {API_KEY}"}

# Bendillinn úr síðustu keyrslu. Dragðu smá frá honum svo pantanir sem
# vistuðust á sömu sekúndu og keyrslan endaði detti ekki niður á milli.
cursor = "2026-05-20T00:00:00Z"

response = requests.get(
    "https://askell.is/api/v2/fulfillment-orders/",
    params={"updated_since": cursor, "status": "open"},
    headers=headers,
)
response.raise_for_status()
orders = response.json()

for order in orders:
    handle_order(order)

if orders:
    # Pantanirnar eru í röð eftir stofnun, ekki uppfærslu, svo taktu hæsta gildið.
    cursor = max(order["updated_at"] for order in orders)

Response

The response contains exactly the same data as the fulfillment_order.* events, so the same processing works for both.

{
  "id": 12345,
  "number": 42,
  "status": "open",
  "contract_id": 12345,
  "billing_run_id": 54321,
  "customer_id": 33482,
  "customer_reference": "customer-12345",
  "customer_name": "Jón Jónsson",
  "customer_email": "jon@example.com",
  "delivery_address": {
    "id": 9876,
    "delivery_name": "Jón Jónsson",
    "address_1": "Hringbraut 12",
    "address_2": "",
    "address_3": "",
    "zip_code": "101",
    "city": "Reykjavík",
    "state": null,
    "country": "IS"
  },
  "shipping_selection": {
    "id": 555,
    "option_id": 7,
    "handler": "posturinn",
    "delivery_mode": "pickup_point",
    "option_name": "Póstbox",
    "service_code": "DPO",
    "price_amount": "940.0000",
    "currency": "ISK",
    "location_id": "9591",
    "location_name": "Póstbox Hallveigarstíg",
    "location_address": "Hallveigarstíg 1 101 Reykjavík",
    "location": {
      "id": "9591",
      "name": "Póstbox Hallveigarstíg",
      "address": "Hallveigarstíg 1 101 Reykjavík",
      "street": "Hallveigarstíg 1",
      "zip_code": "101",
      "town": "Reykjavík",
      "external_id": null,
      "latitude": 64.1,
      "longitude": -21.9
    }
  },
  "lines": [
    {
      "id": 1111,
      "product_id": 222,
      "product_reference": "SKU-1",
      "product_name": "Kaffipoki",
      "quantity": 2,
      "quantity_fulfilled": 0
    }
  ],
  "fulfillments": [
    {
      "id": 333,
      "order_id": 12345,
      "handler": "posturinn",
      "carrier": "posturinn",
      "status": "booked",
      "provider_order_id": "PP123456789",
      "tracking_number": "PP123456789",
      "tracking_url": "",
      "weight_grams": 1000,
      "error": "",
      "booked_at": "2024-01-01T00:00:00Z",
      "created_at": "2024-01-01T00:00:00Z",
      "updated_at": "2024-01-01T00:00:00Z"
    }
  ],
  "estimated_weight_grams": 1000,
  "metadata": {},
  "created_at": "2024-01-01T00:00:00Z",
  "updated_at": "2024-01-01T00:00:00Z"
}

Fields worth noting

Field

Description

delivery_address

A snapshot of the address as it was when the order was created; a later change to the contract does not affect earlier orders.

shipping_selection

The customer’s shipping selection, or null when no carrier is set up.

shipping_selection.location

The pickup location in structured form — ID, name, street, postal code, town and coordinates — or null when the selection has no location.

lines[].product_reference

The seller’s product number (SKU), i.e. what the order is picked by. null if the product has been deleted from the catalog.

fulfillments

Booked shipments, with a tracking number once one is available. An empty list until a shipment is booked.

fulfillments[].handler

The shipping integration the shipment was booked through. An empty string when an external system shipped with a carrier that Áskell has no integration with.

fulfillments[].carrier

Who carried the parcel: the carrier reported by the external system, otherwise handler.

fulfillments[].tracking_url

A URL for tracking the shipment, as reported with fulfill. An empty string when none was reported.

estimated_weight_grams

The estimated total weight, or null if any line is missing a weight — a partial estimate is never returned as the total.

A shipment in the response never includes the carrier’s raw responses (provider_data); among other things, they hold the keys used to fetch the shipping label. What matters for an integration is extracted into the fields above.

Fulfill an order

POST /api/v2/fulfillment-orders/{id}/fulfill/

Reports that the order has shipped from your warehouse. Áskell records the shipment, marks all lines as fulfilled, sets the order to fulfilled, notifies the customer that the order is on its way and sends the fulfillment_order.fulfilled event — exactly as when an order is fulfilled in the dashboard.

The request body can be empty ({}); a seller who delivers the order themselves has no tracking number to record. All fields are optional:

Field

Description

tracking_number

The shipment’s tracking number, at most 128 characters. Included in the notification to the customer.

tracking_url

A URL where the shipment can be tracked. Stored with the shipment.

provider_order_id

The shipment’s ID in the carrier’s system, at most 128 characters.

carrier

The carrier. dropp and posturinn are recognized, and the shipment is filed under that carrier; any other value is stored as plain text.

weight_grams

The weight of the parcel in grams.

{
  "tracking_number": "PP123456789",
  "tracking_url": "https://posturinn.is/track/PP123456789",
  "provider_order_id": "shp_8812",
  "carrier": "posturinn",
  "weight_grams": 1000
}

The response is 200 with the whole order, the same data that GET returns. The shipment is now in the fulfillments list with the status shipped, and quantity_fulfilled on the lines now equals quantity.

The operation is idempotent: if the order has already been fulfilled, it returns 200 with the order unchanged, no new shipment is created, and neither an email nor an event is sent again. A warehouse system that resends a request after a timeout can therefore do so safely.

If the order already has a shipment booked in Áskell (an account that fulfills in Áskell), the tracking number is recorded on that shipment instead of a new one being created.

Cancel an order

POST /api/v2/fulfillment-orders/{id}/cancel/

Cancels an order that will not be shipped, for example when the product is out of stock. The request has no body, and the response is 200 with the order in the cancelled status. A booked shipment that has not yet been dispatched is canceled on the Áskell side at the same time and marked with an explanation — it is not canceled with the carrier; that has to be done with the carrier directly.

This operation is idempotent too: an order that has already been canceled returns 200, and the fulfillment_order.cancelled event is not sent again.

Common errors

Error

HTTP

Description

Not permitted

401

The request did not include an API key.

Not permitted

403

The key is not a secret key, the account does not use subscription contracts, or shipping is not enabled.

Not found

404

No order with this id was found on the account.

Method not allowed

405

The GET endpoints do not accept write requests; use fulfill or cancel.

Invalid data

400

A field in the request failed validation, for example a tracking_number that is too long.

Status conflict

409

The action does not apply to the order’s current status.

The 409 response carries a code that says what stood in the way:

code

Description

order_cancelled

An attempt was made to fulfill an order that had been canceled.

order_fulfilled

An attempt was made to cancel an order that had already been fulfilled.

booking_in_progress

A shipment for the order is being booked in Áskell right now.

The first two are final: the order will not move to where the request intended, and retrying changes nothing. booking_in_progress, on the other hand, is temporary — a booking that Áskell started is still in progress — and the request can simply be retried a moment later.

{
  "error": "The order has been cancelled.",
  "code": "order_cancelled"
}