Social media publishing for e-commerce: automating product launches across Instagram, TikTok, and Facebook

When a new product drops, automatically publish announcements with product images and links across shopping-heavy platforms. Cover catalog integration and dynamic content generation.

Social media publishing for e-commerce: automating product launches across Instagram, TikTok, and Facebook

The product is live, but nobody knows yet

A new product goes live in your store. The inventory is set. The product page is up. The images look good. The price is right.

And then someone opens Instagram. Types a caption. Uploads the first product image. Posts. Switches to Facebook. Types a different caption. Uploads the same image. Posts. Opens TikTok. Creates a short video or photo post. Uploads. Posts. Maybe opens Threads and LinkedIn if the brand is active there too.

By the time the announcement is out on every platform, thirty minutes have passed. For a store that launches one product a month, this is fine. For a store that drops new products weekly — or daily, or in seasonal batches of fifty — this is a full-time job that scales linearly with catalog size.

The product data already exists in a structured format. The images are already hosted. The descriptions are already written. Everything the social post needs is already sitting in a database, waiting to be turned into an announcement. The only thing missing is the automation that connects the catalog to the platforms.

The architecture

┌──────────────────┐
│ E-commerce │ Product created or
│ platform │ status changed to "active"
│ (Shopify, WC, │
│ custom, etc.) │
└────────┬─────────┘
┌──────────────────┐
│ Webhook handler │ Extracts product data,
│ or event │ builds social post,
│ listener │ selects platforms
└────────┬─────────┘
┌──────────────────┐
│ Postproxy API │ Publishes to Instagram,
│ │ TikTok, Facebook, and
│ │ others in one call
└──────────────────┘

The e-commerce platform fires an event when a product is created or published. A handler receives that event, extracts the fields needed for a social post, and calls the Postproxy API. One event, one handler, one API call — and the announcement is live on every platform.

This is the same pattern used for CMS webhooks and blog-to-social automation, adapted for product data instead of articles.

Where the product data comes from

Every e-commerce platform stores product data in a structured format. The fields you need for a social post are almost always available:

  • Title — the product name
  • Description — a short summary or the first sentence of the full description
  • Images — product photos, usually multiple, hosted on the platform’s CDN
  • Price — the current price, sometimes with a compare-at price for sales
  • URL — the product page link
  • Variant info — colors, sizes, materials (optional, useful for specificity)
  • Tags or categories — for hashtags or targeting

The mapping from product data to social post is straightforward. The title becomes the headline. The first image becomes the visual. The description becomes the caption text. The URL becomes the link. The price adds urgency or context.

Shopify

Shopify fires webhooks on products/create and products/update. The payload includes the full product object:

export default async function handler(req, res) {
const product = req.body;
// Only post for newly published products
if (product.status !== "active") {
return res.status(200).json({ skipped: true });
}
const title = product.title;
const description = product.body_html
? stripHtml(product.body_html).substring(0, 200)
: "";
const price = product.variants[0]?.price;
const imageUrl = product.images[0]?.src;
const productUrl = `https://yourstore.com/products/${product.handle}`;
const body = formatProductPost({ title, description, price, productUrl });
const response = await fetch("https://api.postproxy.dev/api/posts", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.POSTPROXY_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
post: { body },
profiles: ["instagram", "tiktok", "facebook"],
media: imageUrl ? [imageUrl] : [],
}),
});
const result = await response.json();
return res.status(200).json(result);
}

Shopify image URLs from cdn.shopify.com are publicly accessible, so they work directly as media URLs for Postproxy. No re-hosting needed.

WooCommerce

WooCommerce sends webhooks on product.created and product.updated. The payload follows the WooCommerce REST API schema:

export default async function handler(req, res) {
const product = req.body;
if (product.status !== "publish") {
return res.status(200).json({ skipped: true });
}
const title = product.name;
const description = product.short_description
? stripHtml(product.short_description).substring(0, 200)
: "";
const price = product.price;
const imageUrl = product.images[0]?.src;
const productUrl = product.permalink;
const body = formatProductPost({ title, description, price, productUrl });
const response = await fetch("https://api.postproxy.dev/api/posts", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.POSTPROXY_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
post: { body },
profiles: ["instagram", "tiktok", "facebook"],
media: imageUrl ? [imageUrl] : [],
}),
});
const result = await response.json();
return res.status(200).json(result);
}

Custom platforms and headless commerce

If you run a custom e-commerce backend or a headless commerce platform (Commerce.js, Medusa, Saleor), the same pattern applies. Fire a webhook or event when a product is published, extract the fields, call the API. The product data schema varies, but the fields you need — title, image, price, URL — exist in every system.

For platforms that do not have built-in webhooks, you can poll the product catalog on a schedule and compare against a list of already-announced products. The same polling pattern used for RSS feeds works for product catalogs.

Formatting product posts

A raw product title and URL is a weak social post. A formatted announcement that includes context — price, a hook, an image — performs significantly better. The formatting is where the difference between “automated” and “automated well” shows up.

The template approach

Start with a simple template and refine it over time:

function formatProductPost({ title, description, price, productUrl }) {
const priceText = price ? ` — $${price}` : "";
const hook = description
? description.split(". ")[0] + "."
: "";
const lines = [
`New drop: ${title}${priceText}`,
];
if (hook) lines.push("", hook);
lines.push("", productUrl);
return lines.join("\n");
}

This produces:

New drop: Merino Wool Crewneck — $89
Lightweight, breathable, and built to last three seasons.
https://yourstore.com/products/merino-wool-crewneck

Short enough for every platform. Includes the product name, price, one sentence of context, and the link. The product image attached via the media field does the heavy lifting visually.

Variations by product type

Not every product needs the same template. A limited-edition drop sounds different from a restocked staple. A sale item needs different framing than a new arrival.

function formatProductPost({ title, description, price, compareAtPrice, productUrl, tags }) {
const isOnSale = compareAtPrice && parseFloat(compareAtPrice) > parseFloat(price);
const isLimitedEdition = tags?.includes("limited-edition");
let headline;
if (isOnSale) {
const discount = Math.round((1 - parseFloat(price) / parseFloat(compareAtPrice)) * 100);
headline = `${title}${discount}% off, now $${price}`;
} else if (isLimitedEdition) {
headline = `Limited drop: ${title} — $${price}`;
} else {
headline = `New: ${title} — $${price}`;
}
const hook = description ? description.split(". ")[0] + "." : "";
const lines = [headline];
if (hook) lines.push("", hook);
lines.push("", productUrl);
return lines.join("\n");
}

This gives you:

  • Sale items: Merino Wool Crewneck — 30% off, now $62
  • Limited editions: Limited drop: Collaboration Jacket — $245
  • Standard new products: New: Merino Wool Crewneck — $89

Small variations in framing, driven by data that already exists in the product catalog. No manual copywriting needed.

Using AI for dynamic captions

For stores that want more natural-sounding posts, an LLM can generate captions from product data. The product title, description, price, and category go in as context. A short, platform-appropriate caption comes back.

async function generateCaption(product) {
const prompt = `Write a short social media announcement (under 200 characters) for this product:
Title: ${product.title}
Description: ${product.description}
Price: $${product.price}
Category: ${product.category}
Be concise and direct. Include the price. Do not use emojis or hashtags.`;
const response = await fetch("https://api.anthropic.com/v1/messages", {
method: "POST",
headers: {
"x-api-key": process.env.ANTHROPIC_API_KEY,
"content-type": "application/json",
"anthropic-version": "2023-06-01",
},
body: JSON.stringify({
model: "claude-haiku-4-5-20251001",
max_tokens: 256,
messages: [{ role: "user", content: prompt }],
}),
});
const result = await response.json();
return result.content[0].text;
}

This fits into the AI content pipeline pattern. The product data is the input. The LLM generates the caption. The Postproxy API handles publishing. The pipeline runs without human involvement for each new product.

For stores that want a review step, save the AI-generated post as a draft and let someone approve it before it goes live.

Multiple product images: carousels

Product pages usually have multiple images — front, back, detail shots, lifestyle photos. On Instagram and Facebook, carousel posts let you include up to 10 images in a single post. This is a natural fit for product launches.

Terminal window
curl -X POST "https://api.postproxy.dev/api/posts" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"post": {
"body": "New: Merino Wool Crewneck — $89\n\nLightweight, breathable, and built to last.\n\nhttps://yourstore.com/products/merino-wool-crewneck"
},
"profiles": ["instagram", "facebook", "threads"],
"media": [
"https://cdn.shopify.com/your-store/products/crewneck-front.jpg",
"https://cdn.shopify.com/your-store/products/crewneck-back.jpg",
"https://cdn.shopify.com/your-store/products/crewneck-detail.jpg",
"https://cdn.shopify.com/your-store/products/crewneck-lifestyle.jpg"
]
}'

Four product images in a single post across three platforms. On Instagram, this becomes a swipeable carousel. On Facebook, a multi-image post. On Threads, a carousel. Postproxy handles the per-platform upload mechanics — Instagram’s container model, Facebook’s photo API, Threads’ container model — from a single array of URLs.

For TikTok, photo posts support up to 35 images. If you include TikTok in the profiles list, the same media array creates a photo post there as well.

Choosing which images to include

Not every product image belongs in a social post. Product pages often include size charts, dimension diagrams, or white-background studio shots that look flat on social media.

If your product data includes image metadata — alt text, position, or tags — you can filter:

function selectSocialImages(images, max = 4) {
return images
.filter((img) => !img.alt?.toLowerCase().includes("size chart"))
.filter((img) => !img.alt?.toLowerCase().includes("dimensions"))
.slice(0, max)
.map((img) => img.src);
}

If there is no metadata, the simplest heuristic is to use the first image (usually the hero shot) for single-image posts, or the first three to four images for carousels. The hero shot carries the most visual weight and is typically the best-styled photo.

Platform-specific considerations

Instagram

Instagram is the primary shopping discovery platform. Product announcements here have the most direct commercial impact.

Links do not work in captions. Instagram does not make URLs in post text clickable. Including a raw URL in the caption looks cluttered and does not drive traffic. Options:

  • Use a “link in bio” reference and update your bio link to point to the new product
  • Use Instagram Shopping tags if your catalog is connected (requires Meta Commerce Manager setup)
  • Add the URL as a first comment, where it is less prominent but still accessible
{
"post": {
"body": "New: Merino Wool Crewneck — $89\n\nLightweight, breathable, and built to last. Link in bio."
},
"profiles": ["instagram"],
"media": ["https://cdn.shopify.com/your-store/products/crewneck-front.jpg"],
"platforms": {
"instagram": {
"first_comment": "Shop now: https://yourstore.com/products/merino-wool-crewneck"
}
}
}

Image requirements. Instagram requires JPEG format. If your product images are PNG (common for transparent backgrounds), they need to be converted before publishing. Minimum resolution is 320x320. Square (1:1), portrait (4:5), and landscape (1.91:1) are supported — carousels crop all images to the first image’s aspect ratio.

Reels. If you have short product videos (under 90 seconds), publishing them as Reels gets significantly more reach than static image posts. Even a simple video — a product rotating on a turntable, a quick unboxing, a lifestyle clip — outperforms static images in Instagram’s algorithm.

TikTok

TikTok is a discovery platform. Users find products they were not looking for. Product announcements that look native to the platform — casual, visual, short — perform better than polished marketing posts.

Photo posts vs video posts. TikTok supports both, but video gets dramatically more reach. If you have product videos, publish those. If you only have photos, a photo post with multiple images works as a slideshow.

Privacy levels. Before publishing to TikTok, Postproxy queries the creator info endpoint to determine available privacy levels. Your post must use a privacy level the creator has enabled. Most business accounts have PUBLIC_TO_EVERYONE available, but this varies.

Disclosure. TikTok requires commercial content to be disclosed. If the post promotes a product you sell, set is_brand_content or is_brand_organic to true as required by TikTok’s policies. Postproxy passes these flags through to TikTok’s API.

Facebook

Facebook Pages remain a significant channel for e-commerce, particularly for stores with an established community.

Link previews. Unlike Instagram, Facebook renders rich link previews from Open Graph tags. Including the product URL in the post text generates a preview card with the product image, title, and description from your product page’s meta tags. This means the link itself drives clicks — the attached image and the link preview work together.

Facebook Shops. If your store is connected to Facebook Commerce Manager, product tags in posts can link directly to your Facebook Shop listings. This keeps the purchase flow inside Facebook rather than sending users to an external site.

Batch launches: handling multiple products at once

Seasonal drops, collection launches, and restocks often involve dozens of products going live simultaneously. Posting each one individually would flood your followers’ feeds and trigger rate limit concerns.

Strategies for batch launches:

Stagger the announcements

Instead of publishing all products the moment they go live, space them out over hours or days. A queue processes one product every 30 minutes, or publishes three per day across a launch week.

async function staggerProductAnnouncements(products) {
const intervalMs = 30 * 60 * 1000; // 30 minutes between posts
for (let i = 0; i < products.length; i++) {
const delay = i * intervalMs;
setTimeout(async () => {
await publishProductPost(products[i]);
}, delay);
}
}

For a more robust approach, use Postproxy’s scheduling feature instead of client-side delays. Set a scheduled_at timestamp for each product post:

async function scheduleProductAnnouncements(products) {
const startTime = new Date();
for (let i = 0; i < products.length; i++) {
const scheduledAt = new Date(startTime.getTime() + i * 30 * 60 * 1000);
await fetch("https://api.postproxy.dev/api/posts", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.POSTPROXY_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
post: { body: formatProductPost(products[i]) },
profiles: ["instagram", "tiktok", "facebook"],
media: [products[i].imageUrl],
scheduled_at: scheduledAt.toISOString(),
}),
});
}
}

All posts are created immediately but published on a schedule. No cron jobs, no client-side timers, no process that needs to stay running.

Collection announcement post

For a large drop, publish a single collection announcement instead of — or in addition to — individual product posts. A carousel showing the hero image from each product, with a caption that links to the collection page.

function formatCollectionPost(collection, products) {
const count = products.length;
const headline = `${collection.title}${count} new pieces, now live`;
const collectionUrl = `https://yourstore.com/collections/${collection.handle}`;
return `${headline}\n\n${collectionUrl}`;
}
const heroImages = products
.slice(0, 10) // Instagram carousel max
.map((p) => p.images[0].src);

One carousel post with up to 10 hero shots is more engaging than ten individual posts and does not overwhelm your followers.

Prioritize hero products

Not every product in a batch deserves its own social post. Pick the hero products — new designs, limited editions, highest-margin items — for individual announcements. Let the rest live in collection posts or on the product page.

function selectHeroProducts(products) {
return products.filter((p) =>
p.tags.includes("hero") ||
p.tags.includes("limited-edition") ||
p.tags.includes("featured")
);
}

Tag products in your catalog system. The automation respects the tags. No manual selection needed at publish time.

Handling sale events and restocks

Product launches are not the only e-commerce event worth announcing. Sales and restocks are equally time-sensitive.

Sale announcements

When a product’s price drops — either through a scheduled sale or a manual discount — the webhook fires with the updated price. Compare against the original price and generate a sale-specific post:

function handleProductUpdate(product) {
const currentPrice = parseFloat(product.variants[0].price);
const compareAt = parseFloat(product.variants[0].compare_at_price);
if (compareAt && compareAt > currentPrice) {
const discount = Math.round((1 - currentPrice / compareAt) * 100);
return {
body: `${product.title}${discount}% off, now $${currentPrice}\n\nhttps://yourstore.com/products/${product.handle}`,
media: [product.images[0].src],
};
}
return null; // Not a sale, skip
}

Restock notifications

For products that sell out and come back, the transition from “out of stock” to “in stock” is the trigger. Shopify does not fire a dedicated restock webhook, but you can detect it by watching products/update events and checking inventory levels:

function isRestock(newProduct, previousState) {
const wasOutOfStock = previousState?.totalInventory === 0;
const isNowInStock = newProduct.variants.some(
(v) => v.inventory_quantity > 0
);
return wasOutOfStock && isNowInStock;
}

Restock announcements carry natural urgency — the product sold out once, and customers who missed it are motivated to act quickly.

Deduplication and safety

Product webhooks can fire multiple times for the same product — once on create, again on update, again when inventory changes. Without deduplication, the same product can generate multiple social posts.

Apply the idempotency strategies covered in our earlier guide:

function generateIdempotencyKey(productId, eventType) {
const today = new Date().toISOString().split("T")[0];
return `product_${productId}_${eventType}_${today}`;
}

This key ensures that the same product can only generate one announcement of each type (launch, sale, restock) per day. If the webhook fires three times for the same product creation, only the first call creates a post.

For stores that want to review posts before they go live, use drafts. The webhook creates a draft post. A team member reviews it in the Postproxy dashboard and publishes when ready.

The complete handler

Here is a full Shopify webhook handler that covers new products, sales, and restocks:

const POSTPROXY_API_KEY = process.env.POSTPROXY_API_KEY;
const STORE_URL = "https://yourstore.com";
// In-memory store; use Redis or a database in production
const publishedKeys = new Set();
export default async function handler(req, res) {
const product = req.body;
const topic = req.headers["x-shopify-topic"];
if (product.status !== "active") {
return res.status(200).json({ skipped: true });
}
let post = null;
let eventType = null;
if (topic === "products/create") {
eventType = "launch";
post = buildLaunchPost(product);
} else if (topic === "products/update") {
const salePost = buildSalePost(product);
if (salePost) {
eventType = "sale";
post = salePost;
}
}
if (!post) {
return res.status(200).json({ skipped: true });
}
// Deduplication
const key = `${product.id}_${eventType}_${new Date().toISOString().split("T")[0]}`;
if (publishedKeys.has(key)) {
return res.status(200).json({ skipped: true, reason: "duplicate" });
}
const response = await fetch("https://api.postproxy.dev/api/posts", {
method: "POST",
headers: {
"Authorization": `Bearer ${POSTPROXY_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(post),
});
if (response.ok) {
publishedKeys.add(key);
}
const result = await response.json();
return res.status(200).json(result);
}
function buildLaunchPost(product) {
const title = product.title;
const price = product.variants[0]?.price;
const imageUrls = product.images.slice(0, 4).map((img) => img.src);
const productUrl = `${STORE_URL}/products/${product.handle}`;
const description = product.body_html
? stripHtml(product.body_html).split(". ")[0] + "."
: "";
const body = description
? `New: ${title} — $${price}\n\n${description}\n\n${productUrl}`
: `New: ${title} — $${price}\n\n${productUrl}`;
return {
post: { body },
profiles: ["instagram", "tiktok", "facebook"],
media: imageUrls,
};
}
function buildSalePost(product) {
const variant = product.variants[0];
const price = parseFloat(variant?.price);
const compareAt = parseFloat(variant?.compare_at_price);
if (!compareAt || compareAt <= price) return null;
const discount = Math.round((1 - price / compareAt) * 100);
const imageUrl = product.images[0]?.src;
const productUrl = `${STORE_URL}/products/${product.handle}`;
return {
post: {
body: `${product.title}${discount}% off, now $${price}\n\n${productUrl}`,
},
profiles: ["instagram", "tiktok", "facebook"],
media: imageUrl ? [imageUrl] : [],
};
}
function stripHtml(html) {
return html.replace(/<[^>]*>/g, "").trim();
}

Deploy this to a serverless platform (Vercel, Netlify Functions, AWS Lambda), configure Shopify webhooks for products/create and products/update pointing at the handler URL, and every new product and sale automatically becomes a social post across Instagram, TikTok, and Facebook.

Using n8n instead of code

For teams that prefer a visual workflow, n8n handles this without writing a handler:

  1. Webhook trigger — receives the Shopify product webhook
  2. IF node — checks product status, determines if it is a launch or sale
  3. Function node — extracts product fields, formats the post body, selects images
  4. HTTP Request node — calls the Postproxy API to publish

Each step is visible, editable, and testable. If you want to add a draft review step before publishing, add a Wait node between formatting and publishing. If you want a Slack notification after the post goes out, add a Slack node at the end.

What Postproxy handles

The e-commerce automation layer you build handles the product data extraction and post formatting. Postproxy handles everything on the publishing side:

  • Media uploads — product images from Shopify’s CDN, WooCommerce’s media library, or any public URL are uploaded to each platform using the correct protocol
  • Carousel creation — multiple product images become carousels on Instagram, Facebook, and Threads automatically
  • Platform constraints — text limits, image format requirements, and video specs are validated before publishing
  • AuthenticationOAuth flows for every platform, including token refresh and re-authentication
  • Per-platform outcomes — you know exactly which platforms succeeded and which failed for every product announcement
  • Scheduling — stagger announcements for batch launches without managing timers in your code

Your catalog is the source of truth. Your handler turns product events into publishing intents. Postproxy turns those intents into live posts on every platform.

Getting started

  1. Connect your social accounts in Postproxy — Instagram, TikTok, Facebook, and whichever other platforms your store is active on
  2. Deploy the webhook handler — a serverless function or n8n workflow that receives product events from your e-commerce platform
  3. Configure product webhooks — point products/create and products/update at your handler URL
  4. Test with a draft product — create a test product, verify the social post looks right, then switch from draft: true to auto-publish

Once wired, every new product, every sale, and every restock becomes a social announcement. The catalog drives the content. The automation drives the distribution. The manual copy-paste-upload-publish cycle is gone.

Ready to get started?

Start with our free plan and scale as your needs grow. No credit card required.