If you are using Shopify or WooCommerce we can provide integrations for you. This API is for customers on other platforms who want to connect to our systems directly.
The API is REST and the following examples are in Python but should be very easy to translate to any other language.
When a call is successful the API returns status code 200 OK (if you are viewing something or changing something) or 201 CREATED (if you have just created something new).
If a call fails you may get 403 FORBIDDEN (which means your secret is wrong), 404 NOT FOUND (if you are trying to view something that doesn’t exist) or 400 BAD REQUEST (for all other expected errors).
A 500 status code means there has been an unexpected error and Move Fresh will have been notified to investigate.
Whenever there is an error you will be sent a JSON response with the error code and a message like this:
{
"error": 10,
"message": "Invalid or missing secret"
}
A full list of error codes appears at the end of this document.
Ping
Just to make sure everything is working you should start with a ping:
import requests
response = requests.get('https://app.movefresh.com/api/ping/',
headers={'Authorization': 'Bearer <<YOUR SECRET>>'}
)
if response.status_code == 200:
print(response.json())
elif response.status_code == 401:
print('Your secret is wrong.')
You should get the message “PONG” coming back.
Create Order
We recommend that orders are pushed to us quite frequently, either by a queue or perhaps every 10 minutes through a scheduled job.
Here’s some example code:
import base64
import requests
# The following two lines are optional.
with open('document.pdf', 'rb') as f:
document = base64.b64encode(f.read()).decode()
response = requests.post('https://app.movefresh.com/api/order/create/',
headers={'Authorization': 'Bearer <<YOUR SECRET>>'},
json={
'order_number': '1001',
'courier': 'dhl_parcel',
'courier_service': '210',
'customer_type': 'consumer', # Optional
'delivery_name': 'Bob Test',
'delivery_oranisation': 'Move Fresh Ltd', # Optional
'delivery_address_1': '2 Rennie Square',
'delivery_address_2': 'Brucefield Ind Estate', # Optional
'delivery_town': 'Livingston',
'delivery_county': 'West Lothian', # Optional
'delivery_postcode': 'EH2 3BU',
'delivery_country': 'GB',
'delivery_email': 'bob@example.com', # Optional
'delivery_phone': '447123456789', # Optional
'delivery_instructions': 'Leave in porch if out', # Optional
'message': 'You now have 100 loyalty points!', # Optional
'priority': 'high', # Optional
'lines': [
{'sku': 'SK1010', 'quantity': 10},
{'sku': 'SK1082', 'quantity': 10}
],
'documents': [document] # Optional
}
)
if response.status_code == 201:
print('Order created successfully.')
elif response.status_code == 400:
print('Error:', response.json()['message'])
elif response.status_code == 500:
print('Unhandled error please contact Move Fresh.')
The fields are as follows:
- order_number – is the Order Number on your store and must be unique and as it is frequently used we would recommend something easily human readable (max 20 characters including brand prefix)
- courier – is ‘dpd’, ‘dhl_parcel’, ‘dhl_parcel_international’, ‘hermes’ or ‘royalmail’
- courier_service – see courier services
- customer_type – is ‘business’ for B2B, or ‘consumer’ for B2C which is the default [optional]
- delivery_name – person’s name (max 60 characters)
- delivery_organisation – company or organisation (max 60 characters) [optional]
- delivery_address_1 – first line of address (max 60 characters)
- delivery_address_2 – second line of address (max 60 characters) [optional]
- delivery_town – town or city (max 60 characters)
- delivery_county – county, province or state (max 60 characters) [optional]
- postcode – postal code or Zip code (max 10 characters)
- delivery_country – ISO-3166 country code (max 2 characters) but note that only countries we ship to are supported as shown below
- delivery_email – an email address for the courier (max 254 characters)[optional]
- delivery_phone – a phone number for the courier (include country code)
- delivery_instructions – a safe place parcels can be left (max 60 characters)[optional]
- message – a message for the customer to be printed on the delivery note (max 128 characters) [optional]
- priority – is ‘low’, ‘normal’ (default if not supplied), ‘high’ or ‘urgent’; it’s recommended that urgent is reserved for manual use to expedite specific orders [optional]
- documents – a list of base64 encoded PDF’s that are to be printed and sent out with the order [optional]
Note that multiple order lines for the same product will be consolidated. So 10 x SKU1 and 20 x SKU1 will be consolidated to 30 x SKU1.
If the courier service is not available it will be adjusted if possible. For example attempting to send to the Scottish Highlands on a next day service will usually be downgraded to a 48 hour service.
Delivery email and phone are used to generate Pre Delivery Notifications (PDN’s) which allow the customer to reschedule deliveries and give them a timed delivery window. The fields are optional but are important for getting first time deliveries. A low rate of first time delivery may result in you paying a higher shipping charge.
The optional documents list allows you to add one or more PDF’s to the order which will be printed out and put in the parcel along with the goods. These might be a personal promotion, a nutrition plan or an invoice.
If successful, the status code 201 CREATED will be returned along with the origin. The origin is your order number with a brand prefix. The brand prefix for “Test Brand” would be “TB” so in this example the order number was 1001 so the origin would be “TB-1001”.
The maximum length of the origin is 20 characters, so in this case the maximum length of the order_number would be 17 characters as the brand prefix “TB-” is three characters.
We make sure that all of our clients have a different brand prefix to avoid confusion.
Linking to Order Page
You may want to link directly to orders from your CRM or other web application. This could be done for an order with the origin TB-1001 by linking to:
https://app.movefresh.com/order/origin/TB-1001/
Cancel Order
If a customer changes their mind or fails a fraud check you may want to cancel the order before it is shipped. The following code would cancel an order with origin TB-1001.
import requests
response = requests.post('https://app.movefresh.com/api/order/TB-1001/cancel/',
headers={'Authorization': 'Bearer <<YOUR SECRET>>'}
)
if response.status_code == 200:
print('Order successfully cancelled.')
elif response.status_code == 404:
print('Order not found.')
elif response.status_code == 400:
print('Order cannot be cancelled')
print(response.json()['message'])
Orders can be cancelled even while being picked but can no longer be cancelled once the pick is complete.
Set Order Priority
After sending an order to Move Fresh it is possible to adjust the priority. This might happen in response to a customer service contact where it is necessary to ship the order as fast as possible.
The possible priorities are ‘low’, ‘normal’, ‘high’ or ‘urgent’. An attempt to set a priority outside of these will generate an error 50.
If an order status is In Process, Done or Cancelled, then it is not possible to update the priority and an error 80 will be returned.
import requests
response = requests.post('https://app.movefresh.com/api/order/TB-1001/priority/',
headers={'Authorization': 'Bearer <<YOUR SECRET>>'},
json={'priority': 'high'}
)
if response.status_code == 200:
print('Order priority updated.')
elif response.status_code == 400:
print(response.json()['message'])
elif response.status_code == 404:
print('Order not found.')
Get Order Status
You may want to check the order status at the end of each day to get the courier tracking details to email to your customers.
In this example we are checking on the order TB-1001 that we created in the first example:
import requests
response = requests.get('https://app.movefresh.com/api/order/TB-1001/',
headers={'Authorization': 'Bearer <<YOUR SECRET>>'}
)
if response.status_code == 200:
result = response.json()
print(result)
elif response.status_code == 404:
print('Order not found.')
The JSON returned will look something like this:
{
"status": "ready",
"message": "Ready to be picked and dispatched"
}
The following statuses may be returned:
- pending – order has just been received but not processed
- waiting – order is waiting for stock to be available
- ready – stock has been allocated and the order is ready to pick
- in_process – is being picked and dispatched
- done – the order is complete
- on_hold – the order has been put on hold by a member of the team
- deferred – certain orders are deferred to be dispatched at a particular time
- cancelled – order has been cancelled and will not dispatched
If the order is done and has been shipped then there will be some extra courier information:
{
"status": "done",
"message": "1 parcel dispatched by DHL Parcel (Next Day YYY/Safe)",
"parcels": 1,
"courier": "dhl_parcel",
"courier_service": "210",
"courier_reference": "123456789",
"courier_link": "https://track.example.com/123456789"
}
Note that the courier link is not always available, depending on how the order was shipped.
Order Webhooks (Beta)
Webhooks can be used to notify your application when an event happens. At the moment webhooks are only available for changes in order status.
A webhook is created by completing the form in the web admin with the URL on your server that you want to be called every time an order is completed.
That URL will get a post containing details of the order in JSON. It will look something like this:
{
"topic": "order/in_process",
"date": "2023-04-03T10:13:27.575900+00:00",
"order": {
"order_number": "1001",
"origin": "TB-1001",
"status": "in_process"
}
}
In the event the order has the status “done” then it will have some extra fields and look like this:
{
"topic": "order/done",
"date": "2023-04-03T10:13:27.575900+00:00",
"order": {
"order_number": "1001",
"origin": "TB-1001",
"status": "done",
"parcels": 1,
"courier": "dhl_parcel",
"courier_service": "210",
"courier_reference": "123456789",
"courier_link": "https://track.example.com/123456789"
}
}
The fields are:
- topic – the topic will either be “order/<< status >>” if the webhook has been setup for that particular status change or “order/any” if the webhook is for all status changes.
- date – is when the webhook was generated in ISO 8601 format. It will always be in UTC. If this is more than a few seconds ago, it indicates that the webhook has been delayed.
- order_number – what was originally supplied when the order was created.
- origin – the order number with your brand prefix added to make it unique in our system.
- status – the current status of the order which is useful if the webhook has been set for all status changes using the “order/any” topic. It is possible for this not to match the topic, if the order status changes as the webhook is being triggered.
- parcels – will usually be one but may be more if a large number of items have been ordered which cannot fit into one box.
- courier and courier_service – will usually be what was specified when the order was created but can sometimes be changed, for example, a next day service may be downgraded to a 48 hour service if the delivery address is in a remote area.
- courier_link – will go to the courier’s website to give up to date tracking information. This link is usually not live until the parcel has arrived in the courier’s depot and been scanned.
It is important to verify the post is genuine. All webhook posts are signed using hash-based message authentication code (HMAC) with SHA-256 using your API secret. The signature is in an HTTP header called X-Hmac-Sha256 which should match the HMAC signature of the body of the post.
Here is a simplified example in Python showing a webhook being received in the Django web framework:
import hashlib
import hmac
import json
from django.views.decorators.csrf import csrf_exempt
@csrf_exempt
def webhook(request):
header = request.headers['X-Hmac-Sha256']
signature = hmac.new(
'<< YOUR SECRET >>'.encode('utf-8'),
msg=request.body,
digestmod=hashlib.sha256
).hexdigest()
if header == signature:
# Signature verified, safe to process.
payload = json.loads(request.body)
Note that the comparison at the end could theoretically expose a timing attack, as the speed of the == operator might reveal how many characters in the header were correct (the more characters that are correct, the slower it will be). It would be better to rewrite with a secure compare method:
if hmac.compare_digest(header, signature):
This method is guaranteed to take exactly the same time to process irrespective of how many characters in the header and signature match.
Once the webhook has been processed your application should return a 200 status code or any other 2xx status code to indicate success. Any content returned will be ignored.
It is important to return the success code quickly. If you need to do slow processing then this should be done after sending the 200 response. The timeout is set to 20 seconds but we would recommend you respond within five seconds.
If there are 20 errors without any successes, then the webhook will be stopped. Any successful 2xx response will reset the error count.
Webhooks are not designed to be completely reliable so your code should check the status of orders that have not been completed once a day to make sure nothing has been missed.
It is also possible that this webhook will be called more than once. Usually this will be because Move Fresh staff have decided to cancel the courier job and create a new one to correct an error.
The User-Agent of the webhook is “Move Fresh Webhook/1.0”. This should not be relied on for security but may be useful for searching web server logs.
Products and Stock
It is good practice to regularly check stock available and update your store.
import requests
response = requests.get('https://app.movefresh.com/api/product/',
headers={'Authorization': 'Bearer <<YOUR SECRET>>'}
)
if response.status_code == 200:
results = response.json()
for product in results['products']:
print(product)
else:
print('Error:', response.status_code)
The JSON returned will be like this:
{'products': [
"name": "Test Product",
"sku": "TESTSKU",
"temperature": "ambient",
"available": 999,
"is_active": true
]}
Temperature can be ‘ambient’, ‘chilled’ or ‘frozen’.
Note that available stock can be negative if we hold more orders than stock.
Create Product
Products can be created through the API which is useful if you have a very large range or regularly add new products.
Note this method is currently in beta and is subject to change.
import requests
response = requests.post('https://app.movefresh.com/api/product/create/',
headers={'Authorization': 'Bearer <<YOUR SECRET>>'},
json={
'name': 'Chicken Pie',
'sku': 'CHICKPIE',
'purchase_sku': 'CHICKPIE', # Optional
'barcode': '900123456',
'category': 'Pies',
'temperature': 'chilled', # Optional
'purchase_units': 1, # Optional
'sell_units': 1, # Optional
'track_lots': True, # Optional
'minimum_life_on_receipt': 30, # Optional
'hide_from_delivery_note': False, # Optional
'weight': 500, # Optional
'volume': 200, # Optional
# The following are required to ship outside the UK.
'customs_value': 3.95,
'hs_code': '16023230',
'country_of_origin': 'GB'
}
)
if response.status_code == 201:
print('Product created successfully.')
The fields are:
- name – is the product name printed on the packaging (max 128 characters)
- sku – a unique Stock Keeping Unit which should match your e-commerce shop, limited to letters, numbers and a hyphen, we recommend you use capital letters (max 15 characters)
- purchase_sku – the SKU used by your supplier, if omitted will be set to the sku (max 15 characters) [optional]
- barcode – the barcode that has been printed on the product (max 20 characters)
- category – the product category, if it does not exist it will be created (max 100 characters)
- temperature – can be one of ‘ambient’, ‘chilled’ or ‘frozen’, use ‘ambient’ if no temperature control is required which is the default [optional]
- purchase_units – the units used to buy from the supplier, for example 18 if you buy in cases of 18, the default is one [optional]
- sell_units – the units used to sell to your customs, for example 10 if you sell in boxes of 10, the default is one [optional]
- track_lots – track Lots and Best Before if the product has a shelf life, default is false [optional]
- minimum_life_on_receipt – minimum days of shelf life required when products are received in the warehouse, 0 for no minimum which is the default [optional]
- hide_from_delivery_note – used for products that should not be printed on the delivery note, default is false [optional]
- weight – weight in g including packaging used to calculate courier rates and for customs declarations [optional, if left blank Move Fresh staff will weigh the product for you]
- volume – in ml or cm³ used to calculate the best box to use [optional, if left blank Move Fresh staff will measure the product for you]
- customs_value – the unit value in GBP for customs declarations [optional unless shipping outside the UK]
- hs_code – the Harmonised System tariff code for customs declarations (max 10 characters) [optional unless shipping outside the UK]
- country_of_origin – ISO-3166 country code (max 2 characters) [optional unless shipping outside the UK]
Harmonised System tariff codes can be looked up at https://www.trade-tariff.service.gov.uk/browse. If you need help finding the right code, working out the customs value or understanding the rules of origin please get in touch with your account manager.
Courier Services
Courier | Courier Service | Description |
DPD | 1^12 | Next Day |
DPD | 1^11 | Two Day |
DPD | 1^16 | Saturday |
DPD | 1^19 | DPD Classic (EU) |
DHL Parcel | 1 | Next Day YYY |
DHL Parcel | 2 | Next Day 12:00 YYY |
DHL Parcel | 4 | Saturday YYY |
DHL Parcel | 9 | Next Day 10:30 YYY |
DHL Parcel | 48 | 48 Hour |
DHL Parcel | 72 | 72 Hour |
DHL Parcel | 210 | Next Day YYY/Safe |
DHL Parcel | 211 | Next Day 12:00 YYY/Safe |
DHL Parcel | 212 | Next Day 10:30 YYY/Safe |
DHL Parcel | 215 | Saturday YYY/Safe |
DHL Parcel | 220 | Next Day NYN |
DHL Parcel | 221 | Next Day 12:00 NYN |
DHL Parcel | 222 | Next Day 10:30 NYN |
DHL Parcel | 225 | Saturday NYN |
DHL Parcel International | 204 | International Road |
Hermes | UK24 | 24 Hour |
Hermes | UK24SIG | 24 Hour Signature |
Hermes | UK48 | 48 Hour |
Hermes | UK48SIG | 48 Hour Signature |
Royal Mail | 1_CRL_F | Royal Mail 24 Large Letter |
Royal Mail | 1_CRL_P | Royal Mail 24 Parcel |
Royal Mail | 2_CRL_F | Royal Mail 48 Large Letter |
Royal Mail | 2_CRL_P | Royal Mail 48 Parcel |
Royal Mail | I_IE1_E | Zone Sort Priority Parcel |
Royal Mail | I_IE1_G | Zone Sort Priority Large Letter |
Royal Mail | I_PS9_E | Max Sort Priority Parcel |
Royal Mail | I_PG9_G | Max Sort Priority Large Letter |
Royal Mail | T_TPN_F | Royal Mail Tracked 24 Large Letter |
Royal Mail | T_TPN_P | Royal Mail Tracked 24 Parcel |
Royal Mail | T_TPS_F | Royal Mail Tracked 48 Large Letter |
Royal Mail | T_TPS_P | Royal Mail Tracked 48 Parcel |
Errors
HTTP Code | HTTP Status | Error | Messsage |
401 | Unauthorized | 10 | Invalid or missing secret. |
400 | Bad Request | 20 | Order <<origin>> has already been imported. |
400 | Bad Request | 30 | No order lines. |
400 | Bad Request | 31 | Invalid SKU: <<sku>> |
400 | Bad Request | 40 | Invalid courier. |
400 | Bad Request | 41 | [Standard|Chilled] courier account does not exist. |
400 | Bad Request | 42 | Courier service <<code>> does not exist. |
400 | Bad Request | 50 | Invalid priority. |
400 | Bad Request | 55 | Invalid temperature. |
400 | Bad Request | 60 | Invalid customer type. |
400 | Bad Request | 70 | Invalid or missing: <<address line>> |
400 | Bad Request | 80 | Order is <<status>> and cannot be updated. |
404 | Not Found | 90 | Order not found. |
405 | Method Not Allowed | 91 | HTTP method <<method>> not allowed use <<method>>. |
400 | Bad Request | 97 | Invalid PDF. |
400 | Bad Request | 98 | Invalid base64 encoding. |
400 | Bad Request | 99 | Invalid JSON. |
500 | Internal Server Error | – | An unhandled error has occurred and no error message will be returned. Move Fresh’s IT team have been notified. |
503 | Service Unavailable | – | Service unavailable due to maintenance. Try again later. |
Supported Countries
GB | United Kingdom |
AU | Australia |
AT | Austria |
BE | Belgium |
CA | Canada |
CN | China |
DK | Denmark |
FI | Finland |
FR | France |
DE | Germany |
GG | Guernsey |
HK | Hong Kong |
HU | Hungary |
IS | Iceland |
IE | Ireland |
IM | Isle of Man |
IT | Italy |
JE | Jersey |
LV | Latvia |
LU | Luxembourg |
MC | Monaco |
NL | Netherlands |
NZ | New Zealand |
NO | Norway |
PL | Poland |
PT | Portugal |
SG | Singapore |
ZA | South Africa |
ES | Spain |
SE | Sweden |
AE | United Arab Emirates |
US | United States |
Postal Code Validation
British and Canadian postal codes are converted to uppercase and have an appropriate space added (if missing) before they are validated.
GB | ^([A-Z][A-HJ-Y]?\d[A-Z\d]? ?\d[A-Z]{2}|GIR ?0A{2})$ |
BE | 4-digit |
CA | ^[ABCEGHJ-NPRSTVXY]\d[ABCEGHJ-NPRSTV-Z] \d[ABCEGHJ-NPRSTV-Z]\d$ |
CN | 6-digit |
FI | 4-digit |
FR | 5-digit |
DE | 5-digit |
HU | 4-digit |
IT | 5-digit |
LV | 4-digit |
LU | 4-digit |
MC | 98000 |
NZ | 4-digit |
NO | 4-digit |
PT | 4-digit |
SG | 6-digit |
ZA | 4-digit |
ES | 5-digit |
SE | 5-digit |
US | ^\d{5}(?:[-\s]\d{4})?$ |