Skip to main content
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

Before You Begin

You’ll need the following credentials from your Remark Dashboard before getting started.
RequirementDescriptionWhere to find it
Vendor IDYour unique Remark identifierDashboard → Settings
API KeyAuthentication for API requests (starts with rmrk_)Dashboard → Settings → API Keys
SFTP CredentialsUsername and SSH key (if using SFTP)Contact support

Quick Start

1

Choose your method

Direct API for small batches (≤1,000 products), SFTP for large product sets.
2

Format your data

JSON objects for API, JSONL files for SFTP.
3

Send or upload

Call the GraphQL mutation or upload to SFTP and trigger processing.
4

Verify import

Check job status to confirm successful processing.

API Endpoint & Authentication

POST https://api.remark.ai/graphql

Authentication Options


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:
ActionFieldValue
Archive productarchivedtrue
Unarchive productarchivedfalse
Unpublish (draft)publishedAtnull
Publish immediatelypublishedAt"2024-01-15T00:00:00Z"
Schedule publishpublishedAtFuture 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

StatusDescription
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.

Data Format

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": [...]
  }
]

Product Schema

Product Fields

FieldTypeRequiredDescription
externalIdstringYesUnique product ID from your system
namestringYes*Product name (max 255 chars)
brandNamestringYes*Brand or manufacturer name
externalUrlstringYes*URL to product page on your store
descriptionstringNoProduct description (HTML supported)
publishedAtISO 8601NoPublication date. null = draft/unpublished
archivedbooleanNotrue to archive, false to unarchive
imagesarrayNoProduct images
variantsarrayNoProduct variants
categoriesarrayNoCategory names (strings)
tagsarrayNoTag 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

FieldTypeRequiredDescription
externalIdstringYes**Unique variant ID from your system
namestringYes*Variant name (e.g., “Large / Blue”)
skustringNoStock keeping unit
upcstringNoUniversal Product Code
inventorynumberNoStock quantity. null = not tracked
inventoryPolicyenumNoCONTINUE or DENY when out of stock
pricesarrayNoPricing 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

FieldTypeRequiredDescription
pricenumberYesCurrent selling price
compareAtPricenumberNoOriginal price (for showing discounts)
currencystringYesISO 4217 code (e.g., USD, EUR, GBP)

Image Fields

FieldTypeRequiredDescription
urlstringYesImage URL (max 2048 chars)
externalIdstringNoUnique ID. Defaults to MD5 hash of URL
positionnumberNoDisplay order (lower = first)

Examples

Full Product Example

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" }]
    }
  ]
}
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 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:
ssh-keygen -t ed25519 -C "[email protected]" -f ~/.ssh/remark_sftp
This creates two files:
  • ~/.ssh/remark_sftpPrivate key (keep secure, never share)
  • ~/.ssh/remark_sftp.pubPublic 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

sftp -i ~/.ssh/remark_sftp [email protected]

Upload Files

sftp> put products-full-2024-01-15.jsonl
sftp> exit

File Naming Conventions

Include date/time and mode for easier debugging:
TypePatternExample
Full syncproducts-full-{date}.jsonlproducts-full-2024-01-15.jsonl
Delta updateproducts-delta-{timestamp}.jsonlproducts-delta-2024-01-15-103000.jsonl
Inventoryinventory-{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.

Data TypeFrequencyMethodMode
Full product syncDaily (overnight)SFTPREPLACE
InventoryEvery 15–60 minSFTP or APIUPDATE
Price changesAs neededSFTP or APIUPDATE
New productsAs neededAPIUPDATE
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

  • Check that publishedAt is set (null = draft/unpublished)
  • Verify required fields: externalId, name, brandName, externalUrl
  • Query the job status and check for errors
  • 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
  • Large files may take several minutes to process
  • Check job status periodically
  • Contact support if stuck for more than 30 minutes
  • Validate each line before upload
  • Run: cat file.jsonl | jq -c . > /dev/null to check for issues
  • Check for trailing commas, unescaped quotes, or multi-line objects

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.