Developers6 min readPro plan

The ResaleOS Developer API

ResaleOS gives you a clean REST API to read and manage your inventory, read your sales, and get notified the moment things change. Push products in from another system, keep an external catalog in sync, or build your own dashboard on top of your store. Here is everything you need to make your first call.

The Developer API is a Pro plan feature. On other plans the API key and webhook settings are visible but locked — upgrade from Settings → Plan to switch it on.

At a glance

What you can build with it

Manage products

List, create, update, and delete inventory — the same catalog you see in the app.

Read sales

Pull orders and line items from every channel and your POS, filtered by date, status, or customer.

Real-time webhooks

Get a signed HTTP callback the instant a product is created, updated, or deleted.

AI on create

Send just a photo URL and ResaleOS writes the title, brand, category, and attributes for you.

Per-item shipping policy

Pick the eBay policy or Etsy profile for one specific item at create time, overriding the channel default.

Step 1

Create an API key

Head to Settings → Integrations → Developer API and click Create API key. Your full key is shown once — copy it somewhere safe. After that ResaleOS only stores a hashed copy and shows the last four characters, so it can never be recovered. Keys look like this:

your API key
ros_live_a1b2c3d4e5f6.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
└── key id ──┘└──────────── secret ────────────┘

Each key is scoped to a single workspace. Lost a key, or someone left the team? Revoke it from the same screen — revoked keys stop working immediately, and you can issue as many as you like.

Step 2

Authenticate every request

The base URL is https://www.resaleos.co/api/v1. Send your key on every request — either as a bearer token or in the X-Api-Key header:

a first request
# List your two most recent active products
curl -s \
  -H "Authorization: Bearer ros_live_abc123..." \
  "https://www.resaleos.co/api/v1/products?status=active&limit=2"

# The X-Api-Key header works too
curl -s \
  -H "X-Api-Key: ros_live_abc123..." \
  "https://www.resaleos.co/api/v1/products"
Reference

Endpoints

Products support full read/write; sales and accounts are read-only. Every list endpoint is paginated and accepts filters like status, search, date ranges, and more. The full parameter list for each endpoint lives in-app under Settings → Integrations → Developer API.

Products

GET
/api/v1/productsList & filter products (paginated)
POST
/api/v1/productsCreate a product — AI fills in the rest from a photo
GET
/api/v1/products/{id}Fetch one product by its resaleosId
PATCH
/api/v1/products/{id}Update one or more fields
DELETE
/api/v1/products/{id}Permanently delete a product

Sales (read-only)

GET
/api/v1/salesList & filter sales / orders (paginated)
GET
/api/v1/sales/{id}Fetch one sale by its saleNumber

Accounts (read-only)

GET
/api/v1/accountsList & filter accounts — consignors, customers, suppliers — with balances (paginated)
GET
/api/v1/accounts/{id}Fetch one account by its accountId

Shipping policies (read-only)

GET
/api/v1/shipping-policies?channel=ebayList your eBay shipping policies or Etsy shipping profiles, with their ids
Example

Create a product from a photo

Send an image URL and a price, and ResaleOS generates the title, brand, category, and attributes for you. Anything you include by hand takes precedence over the AI.

POST /api/v1/products
curl -s -X POST \
  -H "Authorization: Bearer ros_live_abc123..." \
  -H "Content-Type: application/json" \
  "https://www.resaleos.co/api/v1/products" \
  -d '{
    "price": 120,
    "cost": 35,
    "condition": "like_new",
    "status": "active",
    "images": ["https://cdn.example.com/item-front.jpg"],
    "inventory": [{ "location": "YOUR_LOCATION_ID", "quantity": { "available": 1 } }]
  }'
201 Created
{
  "product": {
    "resaleosId": "ROS-00451",
    "title": "Nike Air Max 90 White/Black Low-Top Sneaker",
    "brand": "Nike",
    "status": "active",
    "price": 120,
    "cost": 35,
    "condition": "like_new",
    "attributes": { "color": "White", "size": "10", "material": "Leather" },
    "createdAt": "2026-04-13T14:22:00.000Z"
  },
  "message": "Product created successfully"
}

Grab your location IDs from the same Developer API settings screen — they populate the inventory field.

Example

Category metafields and your own fields

Everything the product editor calls a category metafield lives in one flat attributes object — there is no second endpoint for them. What decides whether a value fills the category's Material field or just sits beside it is the key: use the Shopify taxonomy attribute handle, lowercase and hyphenated. Multi-value fields go in as one comma-separated string.

PATCH /api/v1/products/{id}
{
  "productType": "Wall Art",
  "attributes": {
    "material": "Brass, Wood",          // fills the category's Material field
    "art-movement": "Folk Art",
    "Material": "Brass",                // saves, but as a loose extra field
    "_custom_b4b6c931-a6c6-424f-bab9-4cdffb4c4324": "BIN-221 | FIGURALS"
  }
}

The handles and their allowed values are Shopify's Standard Product Taxonomy, unchanged — open the category in the product editor to see which ones it offers. Values are free text and we map them to each marketplace's own list at export, so the taxonomy's own value names map cleanest.

Your own fields from Settings → Item defaults ride in the same object, keyed _custom_<field id>. To find a field's id, set it on one item in the editor and then GET that item — the _custom_… key in the response is the id, and it never changes. List fields store their values as one comma-separated string, date fields as YYYY-MM-DDTHH:MM:SS, and file fields as { url, fileName }. Custom fields stay internal — they aren't published to a marketplace unless you map them to a channel field.

Sending attributes replaces the whole object, on create as well as update — fetch the product, merge your changes in, and send every key you want to keep.

Example

Set the shipping policy per item

Each sales channel has a default shipping policy, and you can add category rules on top of it. When you already know the right policy for a specific item — a heavier variant of an otherwise identical product, say — send shippingPolicyId on the channel. It applies to that item only and takes priority over both the channel default and any category rule. Supported on eBay (shipping policies) and Etsy (shipping profiles).

GET /api/v1/shipping-policies?channel=ebay
curl -s \
  -H "Authorization: Bearer ros_live_abc123..." \
  "https://www.resaleos.co/api/v1/shipping-policies?channel=ebay"

{
  "channel": "ebay",
  "marketplaceId": "EBAY_US",
  "policies": [
    { "id": "376434832022", "name": "Light-Magazine", "isDefault": true },
    { "id": "376434833022", "name": "Heavy-Magazine", "isDefault": false }
  ]
}
POST /api/v1/products
curl -s -X POST \
  -H "Authorization: Bearer ros_live_abc123..." \
  -H "Content-Type: application/json" \
  "https://www.resaleos.co/api/v1/products" \
  -d '{
    "title": "Motor Trend, May 1967",
    "sku": "MAG-1967-05",
    "price": 39.99,
    "status": "active",
    "channels": [
      { "platform": "storefront" },
      { "platform": "ebay", "shippingPolicyId": "376434833022" }
    ]
  }'

The same block works on PATCH /api/v1/products/{id} to change the policy later — the live listing is updated for you. Send "shippingPolicyId": null to clear the override and go back to the channel default. Every product response echoes the channel back with its current shippingPolicyId.

Sending channels replaces the product's channel selection, so include every channel the item should stay listed on.

Webhooks

Get notified in real time

Instead of polling, register one HTTPS endpoint under Settings → Integrations → Developer API and ResaleOS will POST to it whenever inventory changes or a sale is recorded, updated or refunded. Failed deliveries are retried with backoff, and a persistently unreachable endpoint is auto-disabled so it never becomes a silent hole.

product.createdFires whenever a new product is added to your workspace.
product.updatedFires when any field on a product changes.
product.deletedFires when a product is permanently deleted.
sale.createdFires when a sale is recorded — the register, your storefront checkout, or a marketplace order syncing in. The payload is the same sale object GET /api/v1/sales returns.
sale.updatedFires when a recorded sale changes: an order edited, a payment or invoice collected, or a sale voided. Carries the whole sale as it now stands, not a diff.
sale.refundedFires on a refund, whether issued in ResaleOS or on the marketplace the order came from. Read paymentStatus and amountRefunded for how much of the sale survives.
what we POST to your endpoint
POST https://your-app.example.com/hooks/resaleos
Content-Type: application/json
X-Resaleos-Event: product.created
X-Resaleos-Timestamp: 1755400123
X-Resaleos-Signature: sha256=9f86d081884c7d65...

{
  "event": "product.created",
  "payload": { "resaleosId": "ROS-00421", "title": "Mid-century teak credenza" },
  "timestamp": "1755400123"
}

Verify every payload

Each request carries an X-Resaleos-Signature header — an HMAC-SHA256 of the raw body, keyed with your endpoint signing secret. Recompute it and compare before trusting the event:

verify a webhook (Node.js)
import crypto from 'crypto';

function isValid(rawBody, signatureHeader, signingSecret) {
  const expected =
    'sha256=' +
    crypto.createHmac('sha256', signingSecret).update(rawBody).digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signatureHeader),
  );
}

Requests also include X-Resaleos-Event and X-Resaleos-Timestamp. Rotate your signing secret any time from the settings screen.

Good to know

A few things worth knowing

Keys are hashed

We store only a hashed copy plus the last four characters. A lost key must be revoked and replaced — it can never be shown again.

Workspace-scoped

A key only ever touches the one workspace it was created in. Run multiple stores? Each needs its own key.

Writes sync everywhere

Update a product over the API and it re-syncs to your connected marketplaces and fires the matching webhook, just like an edit in the app.

Deletes are permanent

DELETE removes a product for good — there is no undo. Use status if you only want to archive it.

The complete reference — every query parameter, request field, and a live example response for each endpoint — is built into the app under Settings → Integrations → Developer API, right next to where you create your keys.

Ready to build on ResaleOS?

The Developer API is included on the Pro plan. Get started on Pro for $1 for your first 30 days and create your first API key in minutes. Building something specific? We're happy to help on a quick call.