Import products from CSV
curl --request POST \
--url https://api-eu.flexportal.io/v1/products/csv \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: multipart/form-data' \
--header 'Tenant-ID: <tenant-id>' \
--form csvFile='@example-file'import requests
url = "https://api-eu.flexportal.io/v1/products/csv"
files = { "csvFile": ("example-file", open("example-file", "rb")) }
headers = {
"Tenant-ID": "<tenant-id>",
"Authorization": "Bearer <token>"
}
response = requests.post(url, files=files, headers=headers)
print(response.text)const form = new FormData();
form.append('csvFile', '<string>');
const options = {
method: 'POST',
headers: {'Tenant-ID': '<tenant-id>', Authorization: 'Bearer <token>'}
};
options.body = form;
fetch('https://api-eu.flexportal.io/v1/products/csv', 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-eu.flexportal.io/v1/products/csv",
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=\"csvFile\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001--",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: multipart/form-data",
"Tenant-ID: <tenant-id>"
],
]);
$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-eu.flexportal.io/v1/products/csv"
payload := strings.NewReader("-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"csvFile\"; 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("Tenant-ID", "<tenant-id>")
req.Header.Add("Authorization", "Bearer <token>")
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-eu.flexportal.io/v1/products/csv")
.header("Tenant-ID", "<tenant-id>")
.header("Authorization", "Bearer <token>")
.body("-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"csvFile\"; 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-eu.flexportal.io/v1/products/csv")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Tenant-ID"] = '<tenant-id>'
request["Authorization"] = 'Bearer <token>'
request.body = "-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"csvFile\"; 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{
"success": true,
"message": "<string>",
"created": 123,
"updated": 123,
"errors": [
{
"row": 123,
"error": "<string>"
}
]
}{
"error": {
"code": "<string>",
"message": "<string>"
}
}Products
Import Products CSV
Bulk import products and variants from a CSV file. Supports up to ~500 products per request.
POST
/
v1
/
products
/
csv
Import products from CSV
curl --request POST \
--url https://api-eu.flexportal.io/v1/products/csv \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: multipart/form-data' \
--header 'Tenant-ID: <tenant-id>' \
--form csvFile='@example-file'import requests
url = "https://api-eu.flexportal.io/v1/products/csv"
files = { "csvFile": ("example-file", open("example-file", "rb")) }
headers = {
"Tenant-ID": "<tenant-id>",
"Authorization": "Bearer <token>"
}
response = requests.post(url, files=files, headers=headers)
print(response.text)const form = new FormData();
form.append('csvFile', '<string>');
const options = {
method: 'POST',
headers: {'Tenant-ID': '<tenant-id>', Authorization: 'Bearer <token>'}
};
options.body = form;
fetch('https://api-eu.flexportal.io/v1/products/csv', 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-eu.flexportal.io/v1/products/csv",
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=\"csvFile\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001--",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: multipart/form-data",
"Tenant-ID: <tenant-id>"
],
]);
$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-eu.flexportal.io/v1/products/csv"
payload := strings.NewReader("-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"csvFile\"; 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("Tenant-ID", "<tenant-id>")
req.Header.Add("Authorization", "Bearer <token>")
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-eu.flexportal.io/v1/products/csv")
.header("Tenant-ID", "<tenant-id>")
.header("Authorization", "Bearer <token>")
.body("-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"csvFile\"; 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-eu.flexportal.io/v1/products/csv")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Tenant-ID"] = '<tenant-id>'
request["Authorization"] = 'Bearer <token>'
request.body = "-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"csvFile\"; 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{
"success": true,
"message": "<string>",
"created": 123,
"updated": 123,
"errors": [
{
"row": 123,
"error": "<string>"
}
]
}{
"error": {
"code": "<string>",
"message": "<string>"
}
}Overview
Bulk import products and variants from a CSV file. This is the fastest way to populate your catalog when onboarding or syncing from external systems.This endpoint uses upsert behavior: existing products (matched by Product SKU) are updated, and new products are created. Variants are matched by Variant SKU.
Common Use Cases
Initial Catalog Setup
Quickly populate your catalog when first setting up FlexPortal
Supplier Updates
Import updated pricing or new products from supplier catalogs
Bulk Price Changes
Update pricing across many products at once
Inventory Sync
Periodic sync from inventory management systems
CSV Format
The CSV file must include a header row with these columns:| Column | Required | Description |
|---|---|---|
Product Name | Yes | Display name (e.g., “MacBook Pro 16"") |
Product SKU | Yes | Parent product SKU |
Category | Yes | Product category |
Brand | Yes | Brand/manufacturer |
Specification | Yes | Technical specs (e.g., “M3 Pro, 18GB”) |
Grade | No | Condition: A, B, C, D, E (default: A) |
Color | No | Color option |
Variant SKU | Yes | Unique SKU for this variant |
List Price | Yes | MSRP/retail price |
Acquisition Cost | Yes | Your cost to acquire |
6mo_Price | No | Monthly price for 6-month contract |
12mo_Price | No | Monthly price for 12-month contract |
24mo_Price | No | Monthly price for 24-month contract |
36mo_Price | No | Monthly price for 36-month contract |
Example CSV
Product Name,Product SKU,Category,Brand,Specification,Grade,Color,Variant SKU,List Price,Acquisition Cost,6mo_Price,12mo_Price,24mo_Price,36mo_Price
MacBook Pro 16",MACBOOK-PRO-16,Laptops,Apple,M3 Pro 18GB 512GB,A,Silver,MBP16-M3PRO-18-512-SIL-A,2499,1800,199,159,129,109
MacBook Pro 16",MACBOOK-PRO-16,Laptops,Apple,M3 Pro 18GB 512GB,A,Space Black,MBP16-M3PRO-18-512-BLK-A,2499,1800,199,159,129,109
MacBook Pro 16",MACBOOK-PRO-16,Laptops,Apple,M3 Max 64GB 1TB,A,Space Black,MBP16-M3MAX-64-1TB-BLK-A,3999,2800,319,249,199,169
iPad Pro 12.9",IPAD-PRO-12,Tablets,Apple,M2 256GB WiFi,A,Space Gray,IPAD12-M2-256-WIFI-GRY-A,1099,750,99,79,59,49
ThinkPad X1 Carbon,THINKPAD-X1,Laptops,Lenovo,i7 16GB 512GB,A,Black,X1C-I7-16-512-BLK-A,1899,1200,159,129,99,79
Making the Request
Send the CSV file in the request body withContent-Type: text/csv:
curl -X POST "https://api-eu.flexportal.io/v1/products/csv" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Tenant-ID: YOUR_TENANT_ID" \
-H "Content-Type: text/csv" \
--data-binary @products.csv
const csvContent = await fs.readFile('products.csv', 'utf-8');
const response = await fetch('https://api-eu.flexportal.io/v1/products/csv', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Tenant-ID': 'YOUR_TENANT_ID',
'Content-Type': 'text/csv'
},
body: csvContent
});
const result = await response.json();
console.log(`Imported: ${result.created} created, ${result.updated} updated`);
Response
{
"success": true,
"message": "Import completed",
"summary": {
"totalRows": 50,
"productsCreated": 12,
"productsUpdated": 3,
"variantsCreated": 45,
"variantsUpdated": 5,
"errors": []
}
}
Error Handling
If some rows fail validation, the import continues and returns errors:{
"success": true,
"message": "Import completed with errors",
"summary": {
"totalRows": 50,
"productsCreated": 10,
"variantsCreated": 40,
"errors": [
{
"row": 15,
"sku": "INVALID-SKU",
"error": "Invalid List Price: must be a positive number"
},
{
"row": 23,
"sku": "DUPLICATE-SKU",
"error": "Variant SKU already exists on different product"
}
]
}
}
Best Practices
Validate First
Test with a small sample before importing large catalogs
Consistent SKUs
Use a consistent SKU naming convention for easy management
Include All Pricing
Provide all contract length prices you want to offer
UTF-8 Encoding
Save CSV files with UTF-8 encoding for special characters
Limits
| Limit | Value |
|---|---|
| Maximum file size | 10 MB |
| Maximum rows | 10,000 |
| Processing timeout | 60 seconds |
For very large catalogs (10,000+ products), split into multiple files and import in batches.
Related Endpoints
- List Products - Verify imported products
- Create Product - Add individual products
- List Variants - Query imported variants
Authorizations
API key obtained from FlexPortal dashboard
Headers
Your tenant identifier
Body
multipart/form-data
CSV file with product data
Was this page helpful?