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:
Fetch past orders when an integration is first put into use.
Reconcile regularly and find any orders that are missing.
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 |
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:
A secret API key:
Authorization: Api-Key your-secret-api-key
The account must use V2 subscription contracts.
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 |
|---|---|
|
The order is new and no shipment has been fulfilled yet. |
|
Part of the order has been fulfilled. |
|
The order counts as fulfilled. |
|
The order was canceled. |
Filters
Parameter |
Description |
|---|---|
|
One of the statuses above. |
|
The subscription contract ID. |
|
The customer ID. |
|
The customer reference from your system. |
|
Orders that changed at or after the given time. |
|
Orders created at or after the given time. |
|
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.
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 |
|---|---|
|
A snapshot of the address as it was when the order was created; a later change to the contract does not affect earlier orders. |
|
The customer’s shipping selection, or |
|
The pickup location in structured form — ID, name, street, postal code, town and coordinates — or |
|
The seller’s product number (SKU), i.e. what the order is picked by. |
|
Booked shipments, with a tracking number once one is available. An empty list until a shipment is booked. |
|
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. |
|
Who carried the parcel: the carrier reported by the external system, otherwise |
|
A URL for tracking the shipment, as reported with |
|
The estimated total weight, or |
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 |
|---|---|
|
The shipment’s tracking number, at most 128 characters. Included in the notification to the customer. |
|
A URL where the shipment can be tracked. Stored with the shipment. |
|
The shipment’s ID in the carrier’s system, at most 128 characters. |
|
The carrier. |
|
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 |
|
The request did not include an API key. |
Not permitted |
|
The key is not a secret key, the account does not use subscription contracts, or shipping is not enabled. |
Not found |
|
No order with this |
Method not allowed |
|
The |
Invalid data |
|
A field in the request failed validation, for example a |
Status conflict |
|
The action does not apply to the order’s current status. |
The 409 response carries a code that says what stood in the way:
|
Description |
|---|---|
|
An attempt was made to fulfill an order that had been canceled. |
|
An attempt was made to cancel an order that had already been fulfilled. |
|
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"
}