Remark’s Product Import API allows you to import and synchronize your products directly from your systems. Choose the method that fits your needs:
Import Modes
Replaces your entire product set. Products not included in the import are archived (soft-deleted).Only use REPLACE mode for complete product syncs. Any products missing from your file will be archived.
When to use:
Initial product import
Daily overnight product refresh
When your export represents the complete source of truth
Updates only the products you include. Everything else remains unchanged.When to use:
Inventory updates throughout the day
Price changes
Adding new products
Archiving or unpublishing specific products
Before You Begin
You’ll need the following credentials from your Remark Dashboard before getting started.
Requirement Description Where to find it Vendor ID Your unique Remark identifier Dashboard → Settings API Key Authentication for API requests (starts with rmrk_) Dashboard → Settings → API Keys SFTP Credentials Username and SSH key (if using SFTP) Contact support
Quick Start
Choose your method
Direct API for small batches (≤1,000 products), SFTP for large product sets.
Format your data
JSON objects for API, JSONL files for SFTP.
Send or upload
Call the GraphQL mutation or upload to SFTP and trigger processing.
Verify import
Check job status to confirm successful processing.
API Endpoint & Authentication
POST https://api.remark.ai/graphql
Authentication Options
Use a vendor API key for server-to-server integrations: X-Vendor-Api-Key: {your-api-key}
API keys start with rmrk_ and are scoped to a specific vendor with specific permissions. Use a bearer token for user-authenticated requests: Authorization: Bearer {your-auth-token}
Direct API: Upsert Products
For small to medium batches (up to ~1,000 products), use the upsertProducts mutation.
The direct API always operates in UPDATE mode . Products not included are left unchanged. To archive products, set archived: true. To unpublish, set publishedAt: null.
GraphQL Mutation
mutation UpsertProducts ( $vendorId : ID ! , $products : [ UpsertProductInput ! ] ! ) {
upsertProducts ( vendorId : $vendorId , products : $products ) {
id
externalId
name
}
}
Example Request
curl -X POST https://api.remark.ai/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-auth-token" \
-d '{
"query": "mutation UpsertProducts($vendorId: ID!, $products: [UpsertProductInput!]!) { upsertProducts(vendorId: $vendorId, products: $products) { id externalId } }",
"variables": {
"vendorId": "your-vendor-id",
"products": [
{
"externalId": "prod-001",
"name": "Trail Boots",
"brandName": "SummitGear",
"externalUrl": "https://store.example.com/trail-boots",
"variants": [
{
"externalId": "var-001",
"name": "Size 10",
"prices": [{"price": 189.99, "currency": "USD"}]
}
]
}
]
}
}'
Response
{
"data" : {
"upsertProducts" : [
{ "id" : "uuid-1" , "externalId" : "prod-001" }
]
}
}
Archive & Publish Controls
The direct API supports archiving and publishing controls on each product:
Action Field Value Archive product archivedtrueUnarchive product archivedfalseUnpublish (draft) publishedAtnullPublish immediately publishedAt"2024-01-15T00:00:00Z"Schedule publish publishedAtFuture date
Example: Archive and unpublish products
curl -X POST https://api.remark.ai/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-auth-token" \
-d '{
"query": "mutation UpsertProducts($vendorId: ID!, $products: [UpsertProductInput!]!) { upsertProducts(vendorId: $vendorId, products: $products) { id externalId } }",
"variables": {
"vendorId": "your-vendor-id",
"products": [
{ "externalId": "prod-discontinued", "archived": true },
{ "externalId": "prod-seasonal", "publishedAt": null },
{ "externalId": "prod-preorder", "publishedAt": "2024-06-01T00:00:00Z" }
]
}
}'
Archived vs Unpublished:
archived: true — Soft delete. Product is excluded from all search, recommendations, and AI.
publishedAt: null — Draft mode. Product exists but is not visible to customers.
SFTP File Processing
For large product sets (thousands of products), upload a JSONL file to SFTP, then trigger processing via GraphQL.
Step 1: Upload File to SFTP
sftp -i ~/.ssh/remark_sftp [email protected]
sftp > put products-2024-01-15.jsonl
sftp > exit
Step 2: Trigger Processing
Call the processProductImport mutation to start the import job:
mutation ProcessProductImport ( $input : ProcessProductImportInput ! ) {
processProductImport ( input : $input ) {
jobId
status
}
}
Variables:
{
"input" : {
"files" : [ "products-2024-01-15.jsonl" ],
"mode" : "REPLACE"
}
}
When using API key authentication, vendorId is optional. The vendor is automatically determined from your API key.
Example Request
curl -X POST https://api.remark.ai/graphql \
-H "Content-Type: application/json" \
-H "X-Vendor-Api-Key: rmrk_your-api-key" \
-d '{
"query": "mutation ProcessProductImport($input: ProcessProductImportInput!) { processProductImport(input: $input) { jobId status } }",
"variables": {
"input": {
"files": ["products-2024-01-15.jsonl"],
"mode": "REPLACE"
}
}
}'
Response
{
"data" : {
"processProductImport" : {
"jobId" : "713aa624-1d7c-4e4a-ac23-6463e48a5fdd" ,
"status" : "queued"
}
}
}
Save the jobId to check processing status and debug any issues.
Check Job Status
Query job status to track processing progress:
query ProductImportJob ( $jobId : ID ! ) {
productImportJob ( jobId : $jobId ) {
jobId
status
mode
stats {
productsProcessed
productsCreated
productsUpdated
productsArchived
productsSkipped
errors
}
errorDetails {
line
externalId
message
}
startedAt
completedAt
}
}
Example Request
curl -X POST https://api.remark.ai/graphql \
-H "Content-Type: application/json" \
-H "X-Vendor-Api-Key: rmrk_your-api-key" \
-d '{
"query": "query ProductImportJob($jobId: ID!) { productImportJob(jobId: $jobId) { jobId status mode stats { productsProcessed productsCreated productsUpdated productsArchived errors } errorDetails { line externalId message } completedAt } }",
"variables": {
"jobId": "713aa624-1d7c-4e4a-ac23-6463e48a5fdd"
}
}'
Job Statuses
Status Description queuedJob is waiting to be processed processingImport is in progress completedImport finished successfully failedImport failed — check errorDetails
Example Response
{
"data" : {
"productImportJob" : {
"jobId" : "713aa624-1d7c-4e4a-ac23-6463e48a5fdd" ,
"status" : "completed" ,
"mode" : "REPLACE" ,
"stats" : {
"productsProcessed" : 1523 ,
"productsCreated" : 45 ,
"productsUpdated" : 1475 ,
"productsArchived" : 12 ,
"productsSkipped" : 0 ,
"errors" : 3
},
"errorDetails" : [
{ "line" : 47 , "externalId" : "prod-bad" , "message" : "Missing required field: name" }
],
"completedAt" : "2024-01-15T10:32:45Z"
}
}
}
Partial failures don’t stop processing. Valid products are imported; errors are logged and returned in errorDetails.
Direct API (JSON)
SFTP Files (JSONL)
For the upsertProducts mutation, send an array of product objects: [
{
"externalId" : "prod-001" ,
"name" : "Trail Boots" ,
"brandName" : "SummitGear" ,
"externalUrl" : "https://store.example.com/trail-boots" ,
"variants" : [ ... ]
},
{
"externalId" : "prod-002" ,
"name" : "Rain Jacket" ,
"brandName" : "StormShield" ,
"externalUrl" : "https://store.example.com/rain-jacket" ,
"variants" : [ ... ]
}
]
For file uploads, use JSON Lines format — each line is a complete, valid JSON product: { "externalId" : "prod-001" , "name" : "Trail Boots" , "brandName" : "SummitGear" , "externalUrl" : "https://store.example.com/trail-boots" , "variants" : [ ... ]}
{ "externalId" : "prod-002" , "name" : "Rain Jacket" , "brandName" : "StormShield" , "externalUrl" : "https://store.example.com/rain-jacket" , "variants" : [ ... ]}
{ "externalId" : "prod-003" , "name" : "Hiking Pack" , "brandName" : "TrailMaster" , "externalUrl" : "https://store.example.com/hiking-pack" , "variants" : [ ... ]}
Each line must be valid JSON. No trailing commas, no multi-line objects. A malformed line will be skipped and logged as an error.
Product Schema
Product Fields
Field Type Required Description externalIdstring Yes Unique product ID from your system namestring Yes *Product name (max 255 chars) brandNamestring Yes *Brand or manufacturer name externalUrlstring Yes *URL to product page on your store descriptionstring No Product description (HTML supported) publishedAtISO 8601 No Publication date. null = draft/unpublished archivedboolean No true to archive, false to unarchiveimagesarray No Product images variantsarray No Product variants categoriesarray No Category names (strings) tagsarray No Tag names (strings)
*Required for new products and REPLACE mode. For UPDATE mode on existing products, only externalId and the fields you’re changing are needed.
Variant Fields
Field Type Required Description externalIdstring Yes **Unique variant ID from your system namestring Yes *Variant name (e.g., “Large / Blue”) skustring No Stock keeping unit upcstring No Universal Product Code inventorynumber No Stock quantity. null = not tracked inventoryPolicyenum No CONTINUE or DENY when out of stockpricesarray No Pricing per currency
**If externalId is not provided for a variant, the sku will be used as the external ID. This is convenient if your SKUs are already unique identifiers.
Price Fields
Field Type Required Description pricenumber Yes Current selling price compareAtPricenumber No Original price (for showing discounts) currencystring Yes ISO 4217 code (e.g., USD, EUR, GBP)
Image Fields
Field Type Required Description urlstring Yes Image URL (max 2048 chars) externalIdstring No Unique ID. Defaults to MD5 hash of URL positionnumber No Display order (lower = first)
Examples
A complete product with all fields populated: {
"externalId" : "prod-12345" ,
"name" : "Ultralight Hiking Backpack" ,
"brandName" : "TrailMaster" ,
"description" : "<p>A lightweight 40L backpack perfect for multi-day hikes.</p>" ,
"externalUrl" : "https://store.example.com/products/ultralight-backpack" ,
"publishedAt" : "2024-01-15T00:00:00Z" ,
"images" : [
{ "url" : "https://cdn.example.com/backpack-main.jpg" , "position" : 0 },
{ "url" : "https://cdn.example.com/backpack-side.jpg" , "position" : 1 }
],
"variants" : [
{
"externalId" : "var-sm-blue" ,
"name" : "Small / Blue" ,
"sku" : "TM-ULB-SM-BL" ,
"inventory" : 25 ,
"inventoryPolicy" : "DENY" ,
"prices" : [
{ "price" : 149.99 , "compareAtPrice" : 199.99 , "currency" : "USD" }
]
},
{
"externalId" : "var-md-blue" ,
"name" : "Medium / Blue" ,
"sku" : "TM-ULB-MD-BL" ,
"inventory" : 42 ,
"prices" : [
{ "price" : 159.99 , "currency" : "USD" }
]
}
],
"categories" : [ "Backpacks" , "Hiking Gear" ],
"tags" : [ "ultralight" , "waterproof" ]
}
The minimum required to create a new product: {
"externalId" : "prod-new" ,
"name" : "New Product" ,
"brandName" : "Acme" ,
"externalUrl" : "https://store.example.com/new-product" ,
"variants" : [
{
"externalId" : "var-default" ,
"name" : "Default" ,
"prices" : [{ "price" : 29.99 , "currency" : "USD" }]
}
]
}
Inventory Update (UPDATE)
Update only inventory levels: {
"externalId" : "prod-001" ,
"variants" : [
{ "externalId" : "var-001-sm" , "inventory" : 5 },
{ "externalId" : "var-001-md" , "inventory" : 12 }
]
}
Update pricing for a variant: {
"externalId" : "prod-001" ,
"variants" : [
{
"externalId" : "var-001-sm" ,
"prices" : [
{ "price" : 139.99 , "compareAtPrice" : 179.99 , "currency" : "USD" }
]
}
]
}
Archive a discontinued product: { "externalId" : "prod-discontinued" , "archived" : true }
Unarchive a product: { "externalId" : "prod-discontinued" , "archived" : false }
Unpublish / Publish (UPDATE)
Unpublish a product (set to draft): { "externalId" : "prod-seasonal" , "publishedAt" : null }
Publish immediately: { "externalId" : "prod-seasonal" , "publishedAt" : "2024-01-15T00:00:00Z" }
Schedule future publish: { "externalId" : "prod-preorder" , "publishedAt" : "2024-06-01T00:00:00Z" }
SFTP Setup
Generate SSH Keys
Create an ED25519 key pair for secure authentication:
This creates two files:
~/.ssh/remark_sftp — Private key (keep secure, never share)
~/.ssh/remark_sftp.pub — Public key (send to Remark)
Never share your private key. Only send the .pub file to Remark.
Register Your Key
Email your public key to [email protected] with:
Your company name
Your Remark Vendor ID
Whether this is for staging or production
We’ll confirm when registered and provide your SFTP username.
Connect to SFTP
Upload Files
sftp > put products-full-2024-01-15.jsonl
sftp > exit
File Naming Conventions
Include date/time and mode for easier debugging:
Type Pattern Example Full sync products-full-{date}.jsonlproducts-full-2024-01-15.jsonlDelta update products-delta-{timestamp}.jsonlproducts-delta-2024-01-15-103000.jsonlInventory inventory-{timestamp}.jsonlinventory-2024-01-15-103000.jsonl
Compression
Gzip compression is supported for faster uploads:
gzip products-full-2024-01-15.jsonl
sftp > put products-full-2024-01-15.jsonl.gz
Files on SFTP are retained for 30 days , then automatically deleted.
Recommended Update Cadence
Data Type Frequency Method Mode Full product sync Daily (overnight) SFTP REPLACE Inventory Every 15–60 min SFTP or API UPDATE Price changes As needed SFTP or API UPDATE New products As needed API UPDATE
Use REPLACE mode sparingly — once daily is typically sufficient. For all other updates, use UPDATE mode to avoid accidentally archiving products.
Error Handling
GraphQL Errors
Authentication or permission errors return in the errors array:
{
"errors" : [
{
"message" : "You do not have permission to upsert products" ,
"extensions" : { "code" : "FORBIDDEN" }
}
]
}
Job Errors
When a job completes with errors, check errorDetails for specifics:
{
"data" : {
"productImportJob" : {
"status" : "completed" ,
"stats" : {
"productsProcessed" : 1520 ,
"errors" : 3
},
"errorDetails" : [
{ "line" : 47 , "externalId" : "prod-bad" , "message" : "Missing required field: name" },
{ "line" : 123 , "externalId" : "prod-invalid" , "message" : "Invalid URL format for externalUrl" },
{ "line" : 891 , "externalId" : null , "message" : "Malformed JSON" }
]
}
}
}
Best Practices
Use UPDATE for frequent updates Reserve REPLACE mode for daily syncs. Use UPDATE for inventory, prices, and incremental changes.
Keep external IDs stable Never change a product’s externalId. Changing it creates a duplicate product.
Validate JSONL locally Validate each line is valid JSON before uploading. Use jq or similar tools.
Monitor job status Always check job status after imports to catch and address errors quickly.
Troubleshooting
Products not appearing after import
Check that publishedAt is set (null = draft/unpublished)
Verify required fields: externalId, name, brandName, externalUrl
Query the job status and check for errors
Products unexpectedly archived
This happens with REPLACE mode when products are missing from your file
Ensure your export includes all active products
Use UPDATE mode for partial updates
Verify firewall allows outbound connections to port 22
Check you’re using the correct private key: -i ~/.ssh/remark_sftp
Ensure key permissions: chmod 600 ~/.ssh/remark_sftp
Confirm your public key was registered by Remark
Job stuck in 'processing'
Large files may take several minutes to process
Check job status periodically
Contact support if stuck for more than 30 minutes
GraphQL Schema Reference
# === Inputs ===
input ProcessProductImportInput {
vendorId : ID # Optional with API key auth (derived from key)
files : [ String ! ] !
mode : ProductImportMode !
}
input UpsertProductInput {
externalId : String !
name : String
brandName : String
externalUrl : String
description : String
publishedAt : DateTime
archived : Boolean
images : [ ImageInput ! ]
variants : [ VariantInput ! ]
categories : [ String ! ]
tags : [ String ! ]
}
input VariantInput {
externalId : String # Falls back to sku if not provided
name : String
sku : String
upc : String
inventory : Int
inventoryPolicy : InventoryPolicy
prices : [ PriceInput ! ]
}
input PriceInput {
price : Float !
compareAtPrice : Float
currency : String !
}
input ImageInput {
url : String !
externalId : String
position : Int
}
# === Enums ===
enum ProductImportMode {
REPLACE # Replace entire product set
UPDATE # Update only included products
}
enum ProductImportJobStatus {
queued
processing
completed
failed
}
enum InventoryPolicy {
CONTINUE # Sell when out of stock
DENY # Stop selling when out of stock
}
# === Types ===
type ProductImportJob {
jobId : ID !
status : ProductImportJobStatus !
mode : ProductImportMode !
stats : ProductImportStats
errorDetails : [ ProductImportError ! ]
startedAt : DateTime
completedAt : DateTime
}
type ProductImportStats {
productsProcessed : Int !
productsCreated : Int !
productsUpdated : Int !
productsArchived : Int !
productsSkipped : Int !
errors : Int !
}
type ProductImportError {
line : Int
externalId : String
message : String !
}
# === Mutations ===
type Mutation {
# Direct product upsert (UPDATE mode only)
upsertProducts (
vendorId : ID !
products : [ UpsertProductInput ! ] !
): [ Product ! ] !
# Process SFTP files
processProductImport (
input : ProcessProductImportInput !
): ProductImportJob !
}
# === Queries ===
type Query {
productImportJob ( jobId : ID ! ): ProductImportJob
}
Need Help?
Contact Support Email [email protected] with your Vendor ID, job ID (if applicable), error messages, and sample data.