Skip to content

API integration

The API integration allows vendors without a supported e-commerce platform to connect with a Garnet 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 Garnet > 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:

Full curl command
bash
curl -X POST https://api.garnetmarketplace.com/products \
  -H "Authorization: Bearer your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "externalId": "pineapple-001",
    "title": "Fresh Pineapple",
    "descriptionHtml": "<p>Sweet and juicy tropical pineapple, hand-picked at peak ripeness.</p>",
    "productType": "Fruits",
    "tags": ["tropical", "fresh", "fruit"],
    "variants": [
      {
        "sku": "PINE-SINGLE",
        "price": 4.99,
        "inventoryQuantity": 120,
        "weight": { "value": 900, "unit": "GRAMS" }
      }
    ],
    "images": [
      { "src": "https://images.unsplash.com/photo-1550258987-190a2d41a8ba?w=800", "alt": "Fresh pineapple" }
    ]
  }'

Response:

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

Step 2 — Update stock and price:

Full curl command
bash
curl -X PATCH https://api.garnetmarketplace.com/products \
  -H "Authorization: Bearer your_api_key" \
  -H "Content-Type: application/json" \
  -d '[
    { "sku": "PINE-SINGLE", "price": 3.99, "stock": 85 }
  ]'

Response:

json
{ "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:

FieldTypeRequiredDescription
externalIdstringyesUnique product identifier in the vendor's system
titlestringyesProduct title
descriptionHtmlstringnoProduct description in HTML
vendorstringnoBrand name (defaults to the vendor name)
productTypestringnoProduct type (e.g. "Bags")
tagsstring[]noProduct tags
productOptionsobject[]noProduct options (see below)
variantsobject[]yesAt least one variant (see below)
imagesobject[]noProduct images (see below)
metafieldsobject[]noCustom metafields (see below)

Product option

FieldTypeRequiredDescription
namestringyesOption name (e.g. "Size")
valuesstring[]yesOption values (e.g. ["S", "M", "L"])

Variant

FieldTypeRequiredDescription
skustringyesSKU identifier
barcodestringnoBarcode (EAN, UPC, etc.)
pricenumberyesSale price
compareAtPricenumbernoOriginal price before discount
inventoryQuantityintegernoAvailable stock quantity
trackedbooleannoWhether inventory is tracked (default: true)
requiresShippingbooleannoWhether shipping is required (default: true)
weightobjectno{ value: number, unit: "GRAMS" | "KILOGRAMS" | "OUNCES" | "POUNDS" }
optionValuesobject[]no[{ optionName: "Size", name: "M" }] — must match productOptions
externalVariantIdstringnoUnique variant identifier in the vendor's system

Image

FieldTypeRequiredDescription
srcstringyesPublic URL of the image
altstringnoAlt text for accessibility
externalIdstringnoUnique image ID to avoid re-uploading on updates

Metafield

FieldTypeRequiredDescription
keystringyesMetafield key (e.g. "material")
valuestringyesMetafield value (e.g. "leather")

Example request:

bash
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:

json
{ "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:

FieldTypeRequiredDescription
skustringyesSKU identifier
pricenumberyesNew price
stockintegeryesNew stock quantity

Example request:

bash
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:

json
{ "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:

FieldTypeRequiredDescription
orderIdintegeryesNumeric Shopify order ID (received in the order webhook)
trackingNumberstringyesTracking number
trackingCompanystringnoCarrier name (e.g. "UPS")
trackingUrlstringnoTracking URL

Example request:

bash
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:

json
{ "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

TopicSent when
orders/createA customer places an order containing at least one of the vendor's products
orders/updatedThat 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:

HeaderDescription
Content-Typeapplication/json
X-Garnet-Topicorders/create or orders/updated
X-Garnet-Hmac-Sha256Base64-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.

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

Test payload
json
{
  "id": 5290791567663,
  "admin_graphql_api_id": "gid://shopify/Order/5290791567663",
  "app_id": 580111,
  "browser_ip": "125.208.136.218",
  "buyer_accepts_marketing": false,
  "cancel_reason": null,
  "cancelled_at": null,
  "cart_token": "64e26fd0b2dde9496bac47e76abb1cff",
  "checkout_id": 36641550467375,
  "checkout_token": "104e23f0d8060aa91880be1d3736068c",
  "client_details": {
    "accept_language": "en-SG",
    "browser_height": null,
    "browser_ip": "125.208.136.218",
    "browser_width": null,
    "session_hash": null,
    "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/113.0"
  },
  "closed_at": null,
  "confirmed": true,
  "contact_email": "vendor+test@garnetmarketplace.com",
  "created_at": "2023-04-26T05:13:22-04:00",
  "currency": "SGD",
  "current_subtotal_price": "126.00",
  "current_subtotal_price_set": {
    "shop_money": {
      "amount": "126.00",
      "currency_code": "SGD"
    },
    "presentment_money": {
      "amount": "126.00",
      "currency_code": "SGD"
    }
  },
  "current_total_additional_fees_set": null,
  "current_total_discounts": "0.00",
  "current_total_discounts_set": {
    "shop_money": {
      "amount": "0.00",
      "currency_code": "SGD"
    },
    "presentment_money": {
      "amount": "0.00",
      "currency_code": "SGD"
    }
  },
  "current_total_duties_set": null,
  "current_total_price": "143.58",
  "current_total_price_set": {
    "shop_money": {
      "amount": "143.58",
      "currency_code": "SGD"
    },
    "presentment_money": {
      "amount": "143.58",
      "currency_code": "SGD"
    }
  },
  "current_total_tax": "10.08",
  "current_total_tax_set": {
    "shop_money": {
      "amount": "10.08",
      "currency_code": "SGD"
    },
    "presentment_money": {
      "amount": "10.08",
      "currency_code": "SGD"
    }
  },
  "customer_locale": "en-SG",
  "device_id": null,
  "discount_codes": [],
  "email": "vendor+test@garnetmarketplace.com",
  "estimated_taxes": false,
  "financial_status": "pending",
  "fulfillment_status": null,
  "landing_site": "/",
  "landing_site_ref": null,
  "location_id": null,
  "merchant_of_record_app_id": null,
  "name": "#1030",
  "note": null,
  "note_attributes": [],
  "number": 30,
  "order_number": 1030,
  "order_status_url": "https://ananature.com/74172334383/orders/abec6a39605c6db59aea2a722d285b10/authenticate?key=9662ba1502890060c852ff24c798f355",
  "original_total_additional_fees_set": null,
  "original_total_duties_set": null,
  "payment_gateway_names": ["Cash on Delivery (COD)"],
  "phone": null,
  "presentment_currency": "SGD",
  "processed_at": "2023-04-26T05:13:20-04:00",
  "reference": "305b49b833b6811a84232c0bb3340d73",
  "referring_site": "",
  "source_identifier": "305b49b833b6811a84232c0bb3340d73",
  "source_name": "web",
  "source_url": null,
  "subtotal_price": "126.00",
  "subtotal_price_set": {
    "shop_money": {
      "amount": "126.00",
      "currency_code": "SGD"
    },
    "presentment_money": {
      "amount": "126.00",
      "currency_code": "SGD"
    }
  },
  "tags": "",
  "tax_lines": [
    {
      "price": "10.08",
      "rate": 0.08,
      "title": "GST",
      "price_set": {
        "shop_money": {
          "amount": "10.08",
          "currency_code": "SGD"
        },
        "presentment_money": {
          "amount": "10.08",
          "currency_code": "SGD"
        }
      },
      "channel_liable": false
    }
  ],
  "taxes_included": false,
  "test": false,
  "token": "abec6a39605c6db59aea2a722d285b10",
  "total_discounts": "0.00",
  "total_discounts_set": {
    "shop_money": {
      "amount": "0.00",
      "currency_code": "SGD"
    },
    "presentment_money": {
      "amount": "0.00",
      "currency_code": "SGD"
    }
  },
  "total_line_items_price": "126.00",
  "total_line_items_price_set": {
    "shop_money": {
      "amount": "126.00",
      "currency_code": "SGD"
    },
    "presentment_money": {
      "amount": "126.00",
      "currency_code": "SGD"
    }
  },
  "total_outstanding": "143.58",
  "total_price": "143.58",
  "total_price_set": {
    "shop_money": {
      "amount": "143.58",
      "currency_code": "SGD"
    },
    "presentment_money": {
      "amount": "143.58",
      "currency_code": "SGD"
    }
  },
  "total_shipping_price_set": {
    "shop_money": {
      "amount": "7.50",
      "currency_code": "SGD"
    },
    "presentment_money": {
      "amount": "7.50",
      "currency_code": "SGD"
    }
  },
  "total_tax": "10.08",
  "total_tax_set": {
    "shop_money": {
      "amount": "10.08",
      "currency_code": "SGD"
    },
    "presentment_money": {
      "amount": "10.08",
      "currency_code": "SGD"
    }
  },
  "total_tip_received": "0.00",
  "total_weight": 0,
  "updated_at": "2023-04-26T05:13:23-04:00",
  "user_id": null,
  "billing_address": {
    "first_name": "Frncois",
    "address1": "Tekka Centre",
    "phone": null,
    "city": "Singapore",
    "zip": "123212",
    "province": null,
    "country": "Singapore",
    "last_name": "Rulliere",
    "address2": null,
    "company": null,
    "latitude": null,
    "longitude": null,
    "name": "Frncois Rulliere",
    "country_code": "SG",
    "province_code": null
  },
  "customer": {
    "id": 7012590813487,
    "email": "vendor+test@garnetmarketplace.com",
    "accepts_marketing": false,
    "created_at": "2023-04-26T05:13:21-04:00",
    "updated_at": "2023-04-26T05:13:22-04:00",
    "first_name": "Frncois",
    "last_name": "Rulliere",
    "state": "disabled",
    "note": null,
    "verified_email": true,
    "multipass_identifier": null,
    "tax_exempt": false,
    "phone": null,
    "email_marketing_consent": {
      "state": "not_subscribed",
      "opt_in_level": "single_opt_in",
      "consent_updated_at": null
    },
    "sms_marketing_consent": null,
    "tags": "",
    "currency": "SGD",
    "accepts_marketing_updated_at": "2023-04-26T05:13:21-04:00",
    "marketing_opt_in_level": null,
    "tax_exemptions": [],
    "admin_graphql_api_id": "gid://shopify/Customer/7012590813487",
    "default_address": {
      "id": 9225310241071,
      "customer_id": 7012590813487,
      "first_name": "Frncois",
      "last_name": "Rulliere",
      "company": null,
      "address1": "Tekka Centre",
      "address2": null,
      "city": "Singapore",
      "province": null,
      "country": "Singapore",
      "zip": "123212",
      "phone": null,
      "name": "Frncois Rulliere",
      "province_code": null,
      "country_code": "SG",
      "country_name": "Singapore",
      "default": true
    }
  },
  "discount_applications": [],
  "fulfillments": [],
  "line_items": [
    {
      "id": 13797034197295,
      "admin_graphql_api_id": "gid://shopify/LineItem/13797034197295",
      "fulfillable_quantity": 2,
      "fulfillment_service": "manual",
      "fulfillment_status": null,
      "gift_card": false,
      "grams": 0,
      "name": "Burmanguese Pineapple",
      "price": "19.00",
      "price_set": {
        "shop_money": {
          "amount": "19.00",
          "currency_code": "SGD"
        },
        "presentment_money": {
          "amount": "19.00",
          "currency_code": "SGD"
        }
      },
      "product_exists": true,
      "product_id": 8214156149039,
      "properties": [],
      "quantity": 2,
      "requires_shipping": true,
      "sku": "BURMANGUESE",
      "taxable": true,
      "title": "Burmanguese Pineapple",
      "total_discount": "0.00",
      "total_discount_set": {
        "shop_money": {
          "amount": "0.00",
          "currency_code": "SGD"
        },
        "presentment_money": {
          "amount": "0.00",
          "currency_code": "SGD"
        }
      },
      "variant_id": 44827692302639,
      "variant_inventory_management": "shopify",
      "variant_title": null,
      "vendor": "Sunshine Sweet Produce",
      "tax_lines": [
        {
          "channel_liable": false,
          "price": "3.04",
          "price_set": {
            "shop_money": {
              "amount": "3.04",
              "currency_code": "SGD"
            },
            "presentment_money": {
              "amount": "3.04",
              "currency_code": "SGD"
            }
          },
          "rate": 0.08,
          "title": "GST"
        }
      ],
      "duties": [],
      "discount_allocations": []
    },
    {
      "id": 13797034230063,
      "admin_graphql_api_id": "gid://shopify/LineItem/13797034230063",
      "fulfillable_quantity": 2,
      "fulfillment_service": "manual",
      "fulfillment_status": null,
      "gift_card": false,
      "grams": 0,
      "name": "Brecheche Pineapple",
      "price": "19.00",
      "price_set": {
        "shop_money": {
          "amount": "19.00",
          "currency_code": "SGD"
        },
        "presentment_money": {
          "amount": "19.00",
          "currency_code": "SGD"
        }
      },
      "product_exists": true,
      "product_id": 8214156181807,
      "properties": [],
      "quantity": 2,
      "requires_shipping": true,
      "sku": "BRCHECHE",
      "taxable": true,
      "title": "Brecheche Pineapple",
      "total_discount": "0.00",
      "total_discount_set": {
        "shop_money": {
          "amount": "0.00",
          "currency_code": "SGD"
        },
        "presentment_money": {
          "amount": "0.00",
          "currency_code": "SGD"
        }
      },
      "variant_id": 44807633404207,
      "variant_inventory_management": "shopify",
      "variant_title": null,
      "vendor": "Sunshine Sweet Produce",
      "tax_lines": [
        {
          "channel_liable": false,
          "price": "3.04",
          "price_set": {
            "shop_money": {
              "amount": "3.04",
              "currency_code": "SGD"
            },
            "presentment_money": {
              "amount": "3.04",
              "currency_code": "SGD"
            }
          },
          "rate": 0.08,
          "title": "GST"
        }
      ],
      "duties": [],
      "discount_allocations": []
    },
    {
      "id": 13797034262831,
      "admin_graphql_api_id": "gid://shopify/LineItem/13797034262831",
      "fulfillable_quantity": 1,
      "fulfillment_service": "manual",
      "fulfillment_status": null,
      "gift_card": false,
      "grams": 0,
      "name": "Blue Pineapple",
      "price": "50.00",
      "price_set": {
        "shop_money": {
          "amount": "50.00",
          "currency_code": "SGD"
        },
        "presentment_money": {
          "amount": "50.00",
          "currency_code": "SGD"
        }
      },
      "product_exists": true,
      "product_id": 8262501531951,
      "properties": [],
      "quantity": 1,
      "requires_shipping": true,
      "sku": "green-pineapple",
      "taxable": true,
      "title": "Blue Pineapple",
      "total_discount": "0.00",
      "total_discount_set": {
        "shop_money": {
          "amount": "0.00",
          "currency_code": "SGD"
        },
        "presentment_money": {
          "amount": "0.00",
          "currency_code": "SGD"
        }
      },
      "variant_id": 44959491457327,
      "variant_inventory_management": "shopify",
      "variant_title": null,
      "vendor": "Lampy Electronics Pte Ltd",
      "tax_lines": [
        {
          "channel_liable": false,
          "price": "4.00",
          "price_set": {
            "shop_money": {
              "amount": "4.00",
              "currency_code": "SGD"
            },
            "presentment_money": {
              "amount": "4.00",
              "currency_code": "SGD"
            }
          },
          "rate": 0.08,
          "title": "GST"
        }
      ],
      "duties": [],
      "discount_allocations": []
    }
  ],
  "payment_terms": null,
  "refunds": [],
  "shipping_address": {
    "first_name": "Frncois",
    "address1": "Tekka Centre",
    "phone": null,
    "city": "Singapore",
    "zip": "123212",
    "province": null,
    "country": "Singapore",
    "last_name": "Rulliere",
    "address2": null,
    "company": null,
    "latitude": null,
    "longitude": null,
    "name": "Frncois Rulliere",
    "country_code": "SG",
    "province_code": null
  },
  "shipping_lines": [
    {
      "id": 4327852114223,
      "carrier_identifier": "650f1a14fa979ec5c74d063e968411d4",
      "code": "Expedited",
      "delivery_category": null,
      "discounted_price": "7.50",
      "discounted_price_set": {
        "shop_money": {
          "amount": "7.50",
          "currency_code": "SGD"
        },
        "presentment_money": {
          "amount": "7.50",
          "currency_code": "SGD"
        }
      },
      "phone": null,
      "price": "7.50",
      "price_set": {
        "shop_money": {
          "amount": "7.50",
          "currency_code": "SGD"
        },
        "presentment_money": {
          "amount": "7.50",
          "currency_code": "SGD"
        }
      },
      "requested_fulfillment_service_id": null,
      "source": "shopify",
      "title": "Expedited",
      "tax_lines": [],
      "discount_allocations": []
    }
  ]
}

Error responses

All endpoints return JSON errors in the following format:

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