Skip to content

Selling

Seller quickstart

On this page

Save a listing draft, read it back, and verify an update. You can try these steps in the sandbox without a seller account. Webhook and affiliate setup come after the sandbox exercise, with their own requirements.

A listing is what you send. A product is the record Metzia stores. The API serves buyers in the United States, with prices in US dollars.

Before you startLink to this section

For the TypeScript examples, you need Node.js 20 or later and the @metzia/sdk package from npm. Create a project and install the package before running an SDK example:

mkdir metzia-example
cd metzia-example
npm init -y
npm install @metzia/sdk
npm install --save-dev typescript tsx @types/node

Check the installed version with npx market --version. The source is public at github.com/metziaai/metzia-sdk under the Apache 2.0 license. If you'd rather not install anything, use the curl tabs for the listing exercise. You'll still need the SDK for the webhook examples.

Get a sandbox keyLink to this section

Request one key from the sandbox:

curl --fail-with-body -X POST \
  -H 'content-type: application/json' \
  https://sandbox.metzia.ai/api/sandbox/keys

The response includes key, sellerId, and the permissions catalog:write and catalog:read. You see the key only once. Save it in your current shell without putting it in a source file:

read -r -s -p 'Sandbox key: ' METZIA_KEY
export METZIA_KEY

Paste the returned key at the prompt. Don't commit it or include it in logs. One key works for the whole listing exercise. If the key request returns HTTP 429, wait for the time in Retry-After rather than requesting keys in a loop.

Always pass baseUrl: "https://sandbox.metzia.ai" in this exercise. A sandbox key doesn't work on a production host. The SDK's default URL is not the sandbox URL.

Define a productLink to this section

This example saves a fictional one-time license as a draft. It doesn't publish the product to buyers or start a subscription. A price is in micro-units of USD: 25_000_000 is $25. The SDK converts the number to the API's string format.

Save this complete example as listing.mts:

import { Market } from "@metzia/sdk";
import type { ListingInput, Product } from "@metzia/sdk";

const market = new Market(process.env.METZIA_KEY, { baseUrl: "https://sandbox.metzia.ai" });

const listing: ListingInput = {
  sku: "mailer-pro",
  name: "Mailer Pro",
  categorySlug: "transactional-email",
  pricingModel: "flat",
  delivers: "license",
  provisioning: "keyless",
  summary: "A one-time license for a transactional email tool.",
  notAFitWhen: "It is not a fit for a team that sends bulk marketing campaigns.",
  destinationPath: "/products/mailer-pro",
  pricePoints: [{ amountMicros: 25_000_000, unitName: "per license" }],
};

const saved = await market.products.define(listing);
if (saved.product.status !== "draft" || saved.pendingReview) {
  throw new Error("Expected a sandbox draft.");
}

const page = await market.request<{
  listings: Product[];
  nextCursor: string | null;
}>("/api/v1/products");
if (!page.ok) throw new Error(`Could not read the listing: ${page.code}`);
if (!page.body.listings.some((row) => row.id === saved.product.id)) {
  throw new Error("The saved draft was not returned.");
}

const updated = await market.products.define({ ...listing, name: "Mailer Pro updated" });
if (updated.created || updated.product.id !== saved.product.id) {
  throw new Error("Expected an update of the same product.");
}
if (updated.product.version <= saved.product.version) {
  throw new Error("Expected a newer draft version.");
}

console.log("Verified draft:", saved.product.sku);
console.log("Verified update:", updated.product.name);

Run it from the project directory:

npx tsx listing.mts

Expected output:

Verified draft: mailer-pro
Verified update: Mailer Pro updated

On a fresh sandbox seller, the first call creates a draft. If you run the SDK example again, its first call updates the existing SKU instead. Both paths keep one product for that SKU. A draft isn't visible to buying agents.

The same step with curlLink to this section

Select the curl tab above for the HTTP requests and verification instructions. Generated product IDs and timestamps vary. Compare the returned values rather than copying an example ID.

If a call failsLink to this section

  • For HTTP 401, check the key and the host. A key from before Sunday's reset no longer works.
  • For HTTP 403, check the key's permissions.
  • For a validation error, correct the named field. Don't retry an unchanged invalid listing.
  • After a timeout or uncertain write result, read the listing before sending another update.

The SDK reference lists the errors and retry rules. Catalog fields describes required values, subscriptions, price tiers, and limits.

Handle provisionLink to this section

This is seller integration setup, not a live sandbox purchase exercise. You need the SDK, a webhook signing secret, and your own account-creation code. The following fragment uses createAccount, which you must implement:

import { Market } from "@metzia/sdk";

const market = new Market(process.env.METZIA_KEY, {
  baseUrl: "https://sandbox.metzia.ai",
  webhookSecret: process.env.METZIA_WEBHOOK_SECRET,
});

market.on("provision", async (event) => {
  const account = await createAccount(event.buyerId, event.sku, event.orderId);
  return { externalAccountId: account.id, created: account.created };
});

export const POST = market.webhooks.handler();

Mount POST in a route that accepts Web Request objects, such as a Next.js route handler. Keep the raw request body intact for signature verification.

The event contains buyerId, sku, and orderId. The buyer ID is not an email address. Return your account ID and whether you created the account. Use durable storage to map repeated orderId values to the same account. A repeated provision must return the same account ID with created: false.

Verify the receiver locally with a signed test event. The testing example runs without a platform request. Once your receiver runs locally, use the certification command to check duplicate provision, cancellation, and signature rejection.

Handle cancelLink to this section

Register a cancellation handler on the same client. This fragment requires your own revokeAccount function:

market.on("cancel", async (event) => {
  await revokeAccount(event.externalAccountId, event.sku);
});

The receiver returns { ok: true } after the handler succeeds. Repeating a cancel must leave the account in the same revoked state. See the webhook reference for HTTP responses.

Get paidLink to this section

You get paid at your own checkout. Your affiliate program pays Metzia a commission for eligible referrals. Metzia doesn't take buyer payments, hold balances, or make payouts.

This step requires an approved seller setup and a seller portal key with affiliate_program:read and affiliate_program:write. The sandbox key above has neither permission. Don't use it for this step.

Before moving your integration to production:

  1. Get a seller portal key with the permissions each call needs.
  2. Complete seller approval and have an owner accept the seller agreement.
  3. Set baseUrl to your production host rather than the sandbox host.

Read your program with market.affiliate.get(). Register terms only when the program ID, rate, term, link template, or attribution window needs to change. Don't treat a rate-only comparison as a check of all saved terms.

The affiliate reference shows the parameters, response, permissions, and retry precautions. Your commission rate never buys placement. Ranking inputs explains the fields that determine the order.

Updated 2026-10-14