Skip to content
Dashboard

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_content access 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

1

In your Shopify admin go to Settings → Apps and sales channels → Develop apps.

2

Click Create an app. Name it Brainpercent.

3

Go to Configuration → Admin API integration. Under Admin API access scopes add:

write_content— creates blog articles
4

Click Save, then go to API credentials and click Install app.

5

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.