curl --request POST \
--url https://api.timbrix.mx/products/import \
--header 'Content-Type: multipart/form-data' \
--header 'X-API-Key: <api-key>' \
--form file='@example-file'import requests
url = "https://api.timbrix.mx/products/import"
files = { "file": ("example-file", open("example-file", "rb")) }
headers = {"X-API-Key": "<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-API-Key': '<api-key>'}};
options.body = form;
fetch('https://api.timbrix.mx/products/import', 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.timbrix.mx/products/import",
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-API-Key: <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.timbrix.mx/products/import"
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-API-Key", "<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.timbrix.mx/products/import")
.header("X-API-Key", "<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.timbrix.mx/products/import")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["X-API-Key"] = '<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{
"totalRows": 50,
"successCount": 47,
"errorCount": 3,
"errors": [
{
"row": 3,
"message": "La clave de producto o servicio \"99999999\" no existe en el catálogo SAT (c_ClaveProdServ)"
}
]
}Import Products from CSV
Uploads a CSV file (multipart/form-data, field name “file”) and creates one product per data row. Required columns: description, productKey, price. Optional columns: unitKey (default H87), unitName, sku, taxIncluded, taxability, livemode — same field names as the JSON create endpoint. Each row is validated individually against the SAT product/unit key catalogs before creating it — rows with invalid SAT keys are never created silently. Processing is partial: valid rows are created even if other rows fail, and every failure is reported with its row number and reason.
curl --request POST \
--url https://api.timbrix.mx/products/import \
--header 'Content-Type: multipart/form-data' \
--header 'X-API-Key: <api-key>' \
--form file='@example-file'import requests
url = "https://api.timbrix.mx/products/import"
files = { "file": ("example-file", open("example-file", "rb")) }
headers = {"X-API-Key": "<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-API-Key': '<api-key>'}};
options.body = form;
fetch('https://api.timbrix.mx/products/import', 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.timbrix.mx/products/import",
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-API-Key: <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.timbrix.mx/products/import"
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-API-Key", "<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.timbrix.mx/products/import")
.header("X-API-Key", "<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.timbrix.mx/products/import")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["X-API-Key"] = '<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{
"totalRows": 50,
"successCount": 47,
"errorCount": 3,
"errors": [
{
"row": 3,
"message": "La clave de producto o servicio \"99999999\" no existe en el catálogo SAT (c_ClaveProdServ)"
}
]
}This endpoint takes noorganizationIdin the URL. Authenticate with either a Supabase Bearer session (send theX-Organization-Id: <org-id>header — the user must be a member of that organization) or an API key (the organization resolves automatically from the key; anyX-Organization-Idheader sent alongside an API key is ignored).
Authentication
Accepts either:- A Supabase Bearer session (
Authorization: Bearer <token>) with theX-Organization-Id: <org-id>header — the authenticated user must be a member of that organization. - An API key (
X-API-Key: sk_...) with thewrite:productsscope — the organization is resolved from the key itself.
Request
Content-Type:multipart/form-data
| Field | Type | Required | Description |
|---|---|---|---|
file | file (.csv) | Yes | CSV file, max 5MB. Must have a .csv extension or a text/csv / application/vnd.ms-excel MIME type. |
CSV Columns
The header row is required. Column names match the JSON Create Product field names.| Column | Required | Default | Description |
|---|---|---|---|
description | Yes | — | Product or service description |
productKey | Yes | — | SAT product/service key. Must exist in the SAT c_ClaveProdServ catalog. |
price | Yes | — | Unit price (positive number) |
unitKey | No | "H87" | SAT unit of measure key. Must exist in the SAT c_ClaveUnidad catalog. |
unitName | No | "Elemento" | Unit of measure name |
sku | No | — | Internal product SKU. Must be unique within the organization and within the file. |
taxIncluded | No | false | Whether taxes are already included in the price. Accepts true/false, 1/0, si/no, yes. |
taxability | No | "01" | SAT taxability code (objeto de impuesto) |
livemode | No | true | Whether this is a live mode product. Same accepted boolean values as taxIncluded. |
Example Request
curl -X POST https://api.timbrix.mx/products/import \
-H "Authorization: Bearer <your_token>" \
-H "X-Organization-Id: 550e8400-e29b-41d4-a716-446655440000" \
-F "file=@products.csv"
const result = await timbrix.products.importCsv(
file,
"products.csv",
"550e8400-e29b-41d4-a716-446655440000"
)
console.log(result.successCount, result.errorCount)
X-Organization-Id header (and the SDK’s organizationId argument) entirely — it’s resolved from the key:
const result = await timbrix.products.importCsv(file, "products.csv")
console.log(result.successCount, result.errorCount)
products.csv:
description,productKey,price,unitKey,unitName,sku,taxIncluded,taxability,livemode
Ukelele,60131324,345.60,H87,Elemento,UKL-001,true,01,true
Guitarra acústica,60131327,2500.00,H87,Elemento,GTR-001,true,01,true
Producto inválido,99999999,100.00,,,BAD-001,,,
Example Response
{
"totalRows": 3,
"successCount": 2,
"errorCount": 1,
"errors": [
{
"row": 4,
"message": "La clave de producto o servicio \"99999999\" no existe en el catálogo SAT (c_ClaveProdServ)"
}
]
}
| Field | Type | Description |
|---|---|---|
totalRows | integer | Total data rows processed (excluding the header) |
successCount | integer | Number of products created successfully |
errorCount | integer | Number of rows that failed and did not create a product |
errors | array | Per-row error detail |
errors[].row | integer | 1-based row number as it appears in the file (header = row 1) |
errors[].message | string | Reason the row could not be imported |
Common Errors
Row-level failures (invalid SAT keys, missing required columns in a row, duplicate SKU) do not fail the request — they are reported in theerrors array of a 200 OK response. The errors below abort the whole import instead.
400 Bad Request
No file was uploaded, the file is not a.csv, the file exceeds 5MB, the CSV is empty, the header row is missing a required column (description, productKey, or price), or the X-Organization-Id header is missing on a session-authenticated request.
{
"statusCode": 400,
"message": "El CSV debe incluir las columnas requeridas: description, productKey, price"
}
401 Unauthorized
Missing or invalid Bearer token / API key.403 Forbidden
The authenticated user is not a member of the organization sent inX-Organization-Id, or the API key does not have the write:products scope.Authorizations
API Key for authentication (format: sk_...)
Headers
Required for Supabase session auth. Ignored when authenticating with an API key (the organization resolves from the key).
Body
Response
Import summary with per-row success/error detail
Total de filas de datos procesadas (sin contar el encabezado)
50
Número de productos creados exitosamente
47
Número de filas que fallaron y no generaron un producto
3
Detalle de errores por fila
Show child attributes
Show child attributes