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/nodeCheck 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/keysThe 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_KEYPaste 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.mtsExpected output:
Verified draft: mailer-pro
Verified update: Mailer Pro updatedOn 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:
- Get a seller portal key with the permissions each call needs.
- Complete seller approval and have an owner accept the seller agreement.
- Set
baseUrlto 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