Managing Your Product Catalog
This page covers day-to-day catalog operations: filtering your product list, deactivating items, updating prices, connecting products to payment links, and hosting product images.
These are console-managed operations. The public merchant API uses x-api-key; /api/console/* routes use Privy Bearer tokens and are dashboard-only.
Filtering and Searching
Console
The Products table in the Console supports live search by name and SKU. Use the Active / Archived toggle to switch between live products and soft-deleted ones.
API
The GET /api/console/products endpoint returns only active products. To work with archived products, use the Console UI — the API does not currently expose a filter parameter for active: false.
If you need to find a specific product by SKU in code, filter the list client-side:
1const res = await fetch('https://api.usecoal.xyz/api/console/products', {2 headers: { 'Authorization': 'Bearer <Privy JWT>' },3});4const { products } = await res.json();56const hoodie = products.find((p: { sku: string }) => p.sku === 'MERCH-HOODIE-BLK-L');
Deactivating a Product
Deactivating (deleting) a product is a soft delete — the record is preserved in the database but active is set to false. All Payment Links that reference the product stop resolving immediately.
1curl -X DELETE https://api.usecoal.xyz/api/console/products/clz9prod123 \2 -H "Authorization: Bearer <Privy JWT>"
To reactivate a product, use the Console UI (toggle the archived product back to active). The API does not expose a reactivation endpoint — this is intentional to prevent accidental republication of stale pricing.
Updating Prices
Price changes take effect immediately for new Checkout Sessions. Existing confirmed sessions are unaffected — they retain the amount they were created with.
Via the API:
1curl -X PUT https://api.usecoal.xyz/api/console/products/clz9prod123 \2 -H "Content-Type: application/json" \3 -H "Authorization: Bearer <Privy JWT>" \4 -d '{ "price": 59 }'
Via the Console:
- Open Products and click the product row.
- Edit the price field.
- Click Save Changes.
Tip: If you run a promotional price, consider creating a separate product (e.g.
"Pro Plan — Early Access"at$29) rather than editing the main product. This keeps your analytics clean and lets you easily revert.
Bulk Operations
Coal does not currently expose a bulk API. For large catalog imports, loop over your product list and call POST /api/console/products for each item. Use Promise.allSettled to handle partial failures gracefully:
1const catalog = [2 { name: 'Item A', price: 10, sku: 'SKU-A' },3 { name: 'Item B', price: 20, sku: 'SKU-B' },4 { name: 'Item C', price: 30, sku: 'SKU-C' },5];67const results = await Promise.allSettled(8 catalog.map((item) =>9 fetch('https://api.usecoal.xyz/api/console/products', {10 method: 'POST',11 headers: {12 'Content-Type': 'application/json',13 'Authorization': 'Bearer <Privy JWT>',14 },15 body: JSON.stringify(item),16 }).then((r) => r.json())17 )18);1920const created = results.filter((r) => r.status === 'fulfilled').map((r) => (r as PromiseFulfilledResult<unknown>).value);21const failed = results.filter((r) => r.status === 'rejected');2223console.log(`Created: ${created.length}, Failed: ${failed.length}`);
Rate limiting applies per merchant — stay under 30 requests per minute on the console tier.
Connecting Products to Payment Links
Once a product exists in your catalog you can attach it to a Payment Link in two ways.
Console
- Go to Payment Links → Create Link.
- In the Product dropdown, select the product.
- Click Create — the link immediately resolves to that product's price.
API
Pass productId when creating a link:
1curl -X POST https://api.usecoal.xyz/api/console/links \2 -H "Content-Type: application/json" \3 -H "Authorization: Bearer <Privy JWT>" \4 -d '{5 "productId": "clz9prod123",6 "slug": "pro-plan"7 }'
A product can be attached to multiple payment links simultaneously (e.g., one link shared on Twitter, another embedded in an email). All links share the same product price. Changing the product price updates all linked checkouts at once.
To swap the product on an existing link, delete the link and create a new one — the API does not support patching the productId on an existing link.
Image Hosting Recommendations
Coal does not host product images — you provide a URL. Use a CDN that serves images over HTTPS with low latency.
Uploadthing (recommended for Coal users)
Coal's backend already integrates with Uploadthing. If you upload images through the Console product editor, they are stored via Uploadthing and you receive a stable CDN URL automatically.
For programmatic uploads (e.g., bulk imports), use the Uploadthing SDK:
1import { UTApi } from 'uploadthing/server';23const utapi = new UTApi();45async function uploadProductImage(filePath: string): Promise<string> {6 // Read the file as a Blob/File object7 const file = new File([await Bun.file(filePath).arrayBuffer()], 'product.png', {8 type: 'image/png',9 });1011 const res = await utapi.uploadFiles([file]);12 if (!res[0].data?.url) throw new Error('Upload failed');13 return res[0].data.url;14}
Cloudinary
Cloudinary is a solid alternative with automatic image transformations (resize, format conversion, WebP):
1import { v2 as cloudinary } from 'cloudinary';23cloudinary.config({4 cloud_name: process.env.CLOUDINARY_CLOUD_NAME,5 api_key: process.env.CLOUDINARY_API_KEY,6 api_secret: process.env.CLOUDINARY_API_SECRET,7});89async function uploadToCloudinary(localPath: string): Promise<string> {10 const result = await cloudinary.uploader.upload(localPath, {11 folder: 'coal-products',12 transformation: [{ width: 400, height: 400, crop: 'fill', quality: 'auto' }],13 });14 return result.secure_url;15}
General guidelines
- Use square images. The checkout page renders product images as a square thumbnail. Non-square images are center-cropped.
- Target 400×400 px at 72 DPI for the thumbnail. Provide 800×800 px if you also display the image elsewhere.
- Use WebP or JPEG. PNG is fine but heavier. Avoid GIF and SVG for product thumbnails.
- Use HTTPS. HTTP image URLs will be blocked by the browser on the HTTPS checkout page.
- Keep URLs stable. Changing a product's image URL after creation requires a
PUT /api/console/products/:idcall to update the stored URL.
