Shopify Integration
Auto-publish SEO articles to your Shopify blog using Brainpercent webhooks and Shopify's Admin REST API. Every time Brainpercent generates an article, it can appear as a Shopify blog post — driving organic traffic directly to your store without any manual copy-paste.
What You Need
- A Shopify store (any plan)
- A Shopify Custom App with
write_contentaccess scope - Your Shopify store's blog ID (at least one blog must exist)
- A Brainpercent webhook set to fire on
article.published
Step 1 — Create a Shopify Custom App and Get an Access Token
In your Shopify admin go to Settings → Apps and sales channels → Develop apps.
Click Create an app. Name it Brainpercent.
Go to Configuration → Admin API integration. Under Admin API access scopes add:
write_content— creates blog articlesClick Save, then go to API credentials and click Install app.
Click Reveal token once and copy the Admin API access token. Store it as SHOPIFY_ACCESS_TOKEN.
API version: This guide uses 2024-01. Shopify deprecates API versions quarterly. Check shopify.dev/docs/api/usage/versioning for the current supported versions and update the URL accordingly.
Step 2 — Find Your Shopify Blog ID
Shopify organizes blog posts under Blogs (each blog is a collection of articles). You need the numeric Blog ID to post to the right blog.
curl -s "https://your-store.myshopify.com/admin/api/2024-01/blogs.json" \
-H "X-Shopify-Access-Token: YOUR_ACCESS_TOKEN" | \
python3 -c "import sys,json; [print(b['id'], b['title']) for b in json.load(sys.stdin)['blogs']]"Note the numeric ID for the blog where you want articles published (e.g. 241253187642). If no blog exists yet, create one in Shopify admin under Online Store → Blog posts → Manage blogs.
Step 3 — Webhook Handler
The Shopify endpoint is POST /admin/api/2024-01/blogs/{blog_id}/articles.json. The article body uses body_html — raw HTML is accepted and rendered directly on the Shopify storefront.
import crypto from 'crypto';
import express from 'express';
const app = express();
const SHOPIFY_STORE = process.env.SHOPIFY_STORE; // your-store.myshopify.com
const SHOPIFY_TOKEN = process.env.SHOPIFY_ACCESS_TOKEN;
const SHOPIFY_BLOG = process.env.SHOPIFY_BLOG_ID; // numeric blog ID
const BP_SECRET = process.env.BP_WEBHOOK_SECRET;
function verifySignature(rawBody, signature) {
const expected = crypto
.createHmac('sha256', BP_SECRET)
.update(rawBody)
.digest('hex');
return `sha256=${expected}` === signature;
}
app.post('/webhooks/brainpercent', express.raw({ type: 'application/json' }), async (req, res) => {
if (!verifySignature(req.body, req.headers['x-brainpercent-signature'])) {
return res.status(401).json({ error: 'Invalid signature' });
}
const event = JSON.parse(req.body);
if (event.event !== 'article.published') return res.json({ skipped: true });
const article = event.data;
const shopifyArticle = {
title: article.title,
body_html: article.content_html,
handle: article.slug, // becomes the URL slug
published: true, // false for draft
published_at: article.published_at ?? new Date().toISOString(),
summary_html: article.excerpt ?? '',
...(article.featured_image_url && {
image: {
src: article.featured_image_url,
alt: article.title,
},
}),
tags: (article.tags ?? []).join(', '),
metafields: [
{
key: 'title_tag',
value: article.seo_title ?? article.title,
type: 'single_line_text_field',
namespace: 'global',
},
{
key: 'description_tag',
value: article.seo_description ?? article.excerpt ?? '',
type: 'single_line_text_field',
namespace: 'global',
},
],
};
const shopRes = await fetch(
`https://${SHOPIFY_STORE}/admin/api/2024-01/blogs/${SHOPIFY_BLOG}/articles.json`,
{
method: 'POST',
headers: {
'X-Shopify-Access-Token': SHOPIFY_TOKEN,
'Content-Type': 'application/json',
},
body: JSON.stringify({ article: shopifyArticle }),
}
);
if (!shopRes.ok) {
const err = await shopRes.text();
return res.status(502).json({ error: err });
}
const { article: created } = await shopRes.json();
res.json({ shopify_article_id: created.id, url: created.url });
});
app.listen(3000);SEO Title & Description (Metafields)
Shopify exposes the SEO title and description as metafields under the global namespace with keys title_tag and description_tag. These map to the <title> and <meta name="description"> tags on the article page. They are included in the payload above under metafields.
For metafield details, consult the Shopify developer docs at shopify.dev/docs/apps/custom-data/metafields.
Common Gotchas
- 422 Unprocessable Entity? Usually a duplicate
handle(slug). Shopify requires unique handles per blog. Check for existing articles with the same handle before creating. - Article visible but image not showing? Shopify fetches and re-hosts the image when you pass
image.src. If the Brainpercent image URL is unreachable at creation time (e.g. during a brief propagation window), the image is silently skipped. Retry the image upload separately using the article update endpoint. - Rate limit: 40 requests/app/second (REST API). For high-volume autopilot setups, use a queue and spread requests.
- API version sunset: Shopify retires API versions after ~12 months. Pin the version in your URL and set a calendar reminder to update it. Requests to a sunsetted version are redirected to the oldest supported version which may have breaking changes.
For the full Brainpercent webhook payload schema, see Webhooks & Async guide.