API Integration

Updated on Sep 14, 2026

The API integration allows vendors without a supported e-commerce platform to connect with a Common Goods marketplace via a REST API. Vendors can push products, update prices and stock, submit tracking information, and receive orders via webhook.

Integration scope

  • ✅ Product synchronization (push from vendor)
  • ✅ Price and stock updates
  • ✅ Order synchronization (webhook to vendor)
  • ✅ Tracking synchronization (push from vendor)

Authentication

All API requests must include a Bearer token in the Authorization header. The API key is provided when the marketplace admin creates the integration in > Settings > Integrations > API.

Authorization: Bearer your_api_key

Base URL: https://api.garnetmarketplace.com

Quick start

Here is a full example: create a pineapple product, then update its stock.

Step 1 — Create the product:

 

Response:

{ "productId": "gid://shopify/Product/123456789" }

Step 2 — Update stock and price:

 

Response:

{ "updated": 1, "unknownSkus": [] }

Endpoints

POST /products

Create or update a product in the marketplace. If a product with the same externalId already exists, it will be updated. On first creation, the product is created as DRAFT. The marketplace admin can then approve and publish it.

Request body:

Field Type Required Description
externalId string yes Unique product identifier in the vendor's system
title string yes Product title
descriptionHtml string no Product description in HTML
vendor string no Brand name (defaults to the vendor name)
productType string no Product type (e.g. "Bags")
tags string[] no Product tags
productOptions object[] no Product options (see below)
variants object[] yes At least one variant (see below)
images object[] no Product images (see below)
metafields object[] no Custom metafields (see below)

Product option

Field Type Required Description
name string yes Option name (e.g. "Size")
values string[] yes Option values (e.g. ["S", "M", "L"])

Variant

Field Type Required Description
sku string yes SKU identifier
barcode string no Barcode (EAN, UPC, etc.)
price number yes Sale price
compareAtPrice number no Original price before discount
inventoryQuantity integer no Available stock quantity
tracked boolean no Whether inventory is tracked (default: true)
requiresShipping boolean no Whether shipping is required (default: true)
weight object no { value: number, unit: "GRAMS" | "KILOGRAMS" | "OUNCES" | "POUNDS" }
optionValues object[] no [{ optionName: "Size", name: "M" }] — must match productOptions
externalVariantId string no Unique variant identifier in the vendor's system

Image

Field Type Required Description
src string yes Public URL of the image
alt string no Alt text for accessibility
externalId string no Unique image ID to avoid re-uploading on updates

Metafield

Field Type Required Description
key string yes Metafield key (e.g. "material")
value string yes Metafield value (e.g. "leather")

Example request:

curl -X POST https://api.garnetmarketplace.com/products \
  -H "Authorization: Bearer your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "externalId": "vendor-product-123",
    "title": "Premium Leather Bag",
    "descriptionHtml": "<p>A beautiful leather bag</p>",
    "productType": "Bags",
    "tags": ["leather", "premium"],
    "productOptions": [
      { "name": "Size", "values": ["S", "M", "L"] }
    ],
    "variants": [
      {
        "sku": "BAG-001-S",
        "price": 129.99,
        "inventoryQuantity": 50,
        "optionValues": [{ "optionName": "Size", "name": "S" }]
      },
      {
        "sku": "BAG-001-M",
        "price": 129.99,
        "inventoryQuantity": 30,
        "optionValues": [{ "optionName": "Size", "name": "M" }]
      }
    ],
    "images": [
      { "src": "https://example.com/bag-front.jpg", "alt": "Front view" }
    ],
    "metafields": [
      { "key": "material", "value": "leather" },
      { "key": "country_of_origin", "value": "Italy" }
    ]
  }'

Response:

{ "productId": "gid://shopify/Product/123456789" }

PATCH /products

Bulk update price and stock for existing products by SKU. Maximum 250 items per request. Only updates variants that match existing SKUs in the marketplace.

Request body: Array of objects:

Field Type Required Description
sku string yes SKU identifier
price number yes New price
stock integer yes New stock quantity

Example request:

curl -X PATCH https://api.garnetmarketplace.com/products \
  -H "Authorization: Bearer your_api_key" \
  -H "Content-Type: application/json" \
  -d '[
    { "sku": "BAG-001-S", "price": 119.99, "stock": 45 },
    { "sku": "BAG-001-M", "price": 119.99, "stock": 28 }
  ]'

Response:

{ "updated": 1, "unknownSkus": [] }

POST /tracking

Submit a tracking number for an order. This will fulfill the order on the marketplace and notify the customer.

Request body:

Field Type Required Description
orderId integer yes Numeric Shopify order ID (received in the order webhook)
trackingNumber string yes Tracking number
trackingCompany string no Carrier name (e.g. "UPS")
trackingUrl string no Tracking URL

Example request:

curl -X POST https://api.garnetmarketplace.com/tracking \
  -H "Authorization: Bearer your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "orderId": 5538456789012,
    "trackingNumber": "1Z999AA10123456784",
    "trackingCompany": "UPS",
    "trackingUrl": "https://www.ups.com/track?tracknum=1Z999AA10123456784"
  }'

Response:

{ "fulfilled": true }

Webhooks

When a customer places an order on the marketplace, Garnet sends it to the vendor's system as a webhook: an HTTP POST request to the vendor's registered webhook URL, with the order as JSON payload.

WARNING

Webhook registration is not self-service yet. Contact us with the URL you want orders delivered to, you will receive in return the webhook secret used to verify deliveries.

Topics

Topic Sent when
orders/create A customer places an order containing at least one of the vendor's products
orders/updated That order is later updated (payment, cancellation, refund, address change, edit)

The order only contains the vendor's own line items: an order spanning several vendors is split, and each vendor receives their share only.

Each delivery includes the following headers:

Header Description
Content-Type application/json
X-Garnet-Topic orders/create or orders/updated
X-Garnet-Hmac-Sha256 Base64-encoded signature of the payload (see below)

Respond with a 2xx status code to acknowledge the delivery. Any other response is treated as a failed delivery and reported to the Garnet team.

Verifying a webhook

Before processing a delivery, verify it was sent by Garnet and not by a third party. Compute the HMAC-SHA256 digest of the raw request body using your webhook secret as the key, base64-encode it, and compare it to the X-Garnet-Hmac-Sha256 header using a constant-time comparison. If they differ, reject the request with a 401 status.

import crypto from 'crypto';

function verifyWebhook(rawBody, hmacHeader, secret) {
  const digest = crypto.createHmac('sha256', secret).update(rawBody, 'utf8').digest('base64');
  return crypto.timingSafeEqual(Buffer.from(digest, 'base64'), Buffer.from(hmacHeader, 'base64'));
}

TIP

The digest must be computed on the raw request body, exactly as received — before any JSON parsing or re-serialization. Most frameworks need to be configured to expose the raw body (e.g. express.raw() or the verify option of express.json()).

Order payload

The payload follows the Shopify Order datatype — the exact same format Shopify uses for its own orders/create and orders/updated webhooks.

 

Error responses

All endpoints return JSON errors in the following format:

{ "error": "Error message describing what went wrong" }
Status Description
400 Invalid request (validation error)
401 Invalid or missing API key
404 Resource not found
500 Server error