curl --request POST \
--url https://api.loopreturns.com/api/v1/bulk-operations \
--header 'Content-Type: multipart/form-data' \
--header 'X-Authorization: <api-key>' \
--form file='@example-file'import requests
url = "https://api.loopreturns.com/api/v1/bulk-operations"
files = { "file": ("example-file", open("example-file", "rb")) }
headers = {"X-Authorization": "<api-key>"}
response = requests.post(url, files=files, headers=headers)
print(response.text)const form = new FormData();
form.append('file', '<string>');
const options = {method: 'POST', headers: {'X-Authorization': '<api-key>'}};
options.body = form;
fetch('https://api.loopreturns.com/api/v1/bulk-operations', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.loopreturns.com/api/v1/bulk-operations",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => "-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"file\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001--",
CURLOPT_HTTPHEADER => [
"Content-Type: multipart/form-data",
"X-Authorization: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.loopreturns.com/api/v1/bulk-operations"
payload := strings.NewReader("-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"file\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001--")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("X-Authorization", "<api-key>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.loopreturns.com/api/v1/bulk-operations")
.header("X-Authorization", "<api-key>")
.body("-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"file\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001--")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.loopreturns.com/api/v1/bulk-operations")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["X-Authorization"] = '<api-key>'
request.body = "-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"file\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001--"
response = http.request(request)
puts response.read_body{
"bulk_operation": {
"id": 641174584320376000,
"status": "pending",
"pending_count": 1,
"failed_count": 0,
"success_count": 0,
"created_at": "2023-04-25T13:25:00-05:00",
"updated_at": "2023-04-27T13:25:00-05:00"
}
}{
"errors": "Unauthorized."
}{
"message": "The file field is required.",
"errors": {
"file": [
"The file field is required."
]
}
}Create Bulk Operation
Create a new bulk operation.
curl --request POST \
--url https://api.loopreturns.com/api/v1/bulk-operations \
--header 'Content-Type: multipart/form-data' \
--header 'X-Authorization: <api-key>' \
--form file='@example-file'import requests
url = "https://api.loopreturns.com/api/v1/bulk-operations"
files = { "file": ("example-file", open("example-file", "rb")) }
headers = {"X-Authorization": "<api-key>"}
response = requests.post(url, files=files, headers=headers)
print(response.text)const form = new FormData();
form.append('file', '<string>');
const options = {method: 'POST', headers: {'X-Authorization': '<api-key>'}};
options.body = form;
fetch('https://api.loopreturns.com/api/v1/bulk-operations', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.loopreturns.com/api/v1/bulk-operations",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => "-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"file\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001--",
CURLOPT_HTTPHEADER => [
"Content-Type: multipart/form-data",
"X-Authorization: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.loopreturns.com/api/v1/bulk-operations"
payload := strings.NewReader("-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"file\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001--")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("X-Authorization", "<api-key>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.loopreturns.com/api/v1/bulk-operations")
.header("X-Authorization", "<api-key>")
.body("-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"file\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001--")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.loopreturns.com/api/v1/bulk-operations")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["X-Authorization"] = '<api-key>'
request.body = "-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"file\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001--"
response = http.request(request)
puts response.read_body{
"bulk_operation": {
"id": 641174584320376000,
"status": "pending",
"pending_count": 1,
"failed_count": 0,
"success_count": 0,
"created_at": "2023-04-25T13:25:00-05:00",
"updated_at": "2023-04-27T13:25:00-05:00"
}
}{
"errors": "Unauthorized."
}{
"message": "The file field is required.",
"errors": {
"file": [
"The file field is required."
]
}
}Authorizations
API Scope: "Bulk Operations (write)"
Body
The JSONL file containing the data for the bulk operations.
Each line contains an individual operation, formatted as follows:
{"type": "{job type}", "parameters": {}, "payload": {}}
The following job types are supported:
order_createorder_updateorder_upsertorder_deleteproduct_createproduct_updateproduct_upsertproduct_deleteproduct_variant_createproduct_variant_updateproduct_variant_upsertproduct_variant_deletecollection_createcollection_updatecollection_upsertcollection_deleteinventory_upsertinventory_delete
Addressing records in job parameters
The following delete and upsert job types accept an optional addressing
parameter that controls how the target record is identified:
global(default) — use Loop global IDs inparametersexternal— use your platform's external IDs inparameters
When addressing is omitted, these jobs default to global. An invalid
addressing value causes the job to fail with a validation error. If the
external ID does not resolve to an existing record, the job fails as not found.
For each supported job type, Global is the default parameters shape.
External applies when addressing is "external":
order_delete,product_delete,collection_delete- Global:
id - External:
externalId
- Global:
product_variant_delete- Global:
id,productId - External:
productExternalId,externalId
- Global:
product_variant_upsert- Global:
productId - External:
productExternalId
- Global:
inventory_upsert,inventory_delete- Global:
productVariantGlobalId,locationGlobalId - External:
variantExternalId,locationExternalId
- Global:
order_upsert, product_upsert, and collection_upsert always match by
external_id in the payload and do not use an addressing parameter.
Create and update jobs use only global addressing (for example, id or
productId in parameters).
For inventory_upsert and inventory_delete with addressing set to
"external", if more than one product variant in the shop matches
variantExternalId, the job fails with a conflict error.
Payload details can be found in the documentation for each job, linked above.
Example job lines
Examples below are pretty-printed for readability. In the JSONL file, each job must be a single line with no embedded newlines.
Update a product by Loop global ID (default addressing)
{
"type": "product_update",
"parameters": {
"id": 1234567890
},
"payload": {
"external_id": "product-example-id",
"sku": "LOOP-12345",
"name": "Example",
"weight_grams": 500,
"barcode": "Updated-Barcode",
"description": "An example product",
"type": "Example Product Type",
"vendor": "Internal",
"price": {
"currency_code": "USD",
"amount": 100
},
"created_at": "2023-08-10T15:00:00+00:00",
"updated_at": "2023-08-12T15:00:00+00:00",
"source": "SFCC",
"sales_channel": "Online Store",
"options": [
{
"position": 1,
"name": "Color",
"values": ["red", "green", "blue"]
},
{
"position": 2,
"name": "Condition",
"values": ["new", "used", "refurbished"]
}
],
"images": [
"https://example.com/image.jpg",
"https://example.com/image.png"
],
"tags": ["Socks", "Sandals", "Sunglasses"]
}
}
Delete an order by external ID
{
"type": "order_delete",
"parameters": {
"addressing": "external",
"externalId": "shopify-order-1001"
},
"payload": {}
}
Upsert a product variant by external product ID
{
"type": "product_variant_upsert",
"parameters": {
"addressing": "external",
"productExternalId": "prod-001"
},
"payload": {
"external_id": "var-red-m",
"name": "Red / Medium"
}
}
Upsert inventory by external IDs
{
"type": "inventory_upsert",
"parameters": {
"addressing": "external",
"variantExternalId": "4006381333931",
"locationExternalId": "main-warehouse"
},
"payload": {
"available_count": 42
}
}
Delete inventory by external IDs
{
"type": "inventory_delete",
"parameters": {
"addressing": "external",
"variantExternalId": "4006381333931",
"locationExternalId": "main-warehouse"
},
"payload": {}
}
Lines must be separated with \n.
There is no limit to the number of lines that can be included in a file, but bulk operation files are limited to 2 MB in size.
Response
Success
Show child attributes
Show child attributes