You can replace Jetpack’s newsletter signup UI — the Subscribe block or the popup — with your own form, and keep driving the exact same WordPress.com subscription backend. This post explains how the integration works.
Transparency note: this post was written by GLM 5.3 Flash working in the pi coding agent. There may be inaccuracies — verify against your own setup before relying on it.
Why custom UI?
Both stock options work, but the popup interrupts readers with a modal, and the block ships its own markup with only a few style knobs — so it’s not as easy to style as home-baked code if you’re writing your theme from scratch. With your own UI, the form is fully yours: your fonts, colors, focus states, light/dark mode. The backend (subscriber management, confirmation emails, unsubscribe links, subscriber counts) stays Jetpack’s.
What the Jetpack UI does under the hood
The popup (and the block, once JavaScript loads) is an iframe over a hosted page:
https://subscribe.wordpress.com/memberships/?email={address}&blog={blog_id}&plan=newsletter&display=alternate
That page is site-branded, collects the email, and hands it to a server-side subscribe call. The key fact: for free newsletters, the server acts on that request directly. A plain GET with the right parameters creates the pending subscriber and queues the confirmation email. No nonce, no OAuth, no JavaScript.
The integration
That makes the whole flow a plain HTML form — style it however you like:
<form action="https://subscribe.wordpress.com/memberships/" method="get"> <input type="email" name="email" required /> <input type="hidden" name="blog" value="{BLOG_ID}" /> <input type="hidden" name="plan" value="newsletter" /> <input type="hidden" name="display" value="alternate" /> <input type="hidden" name="source" value="https://example.com/" /> <button type="submit">Subscribe</button></form>
After submit, WordPress.com emails the subscriber a confirmation link containing a magic-login token plus a subscription activate key, and on click flips the subscriber to confirmed and sends the welcome email.
Your own status messages, not the interstitial
Without JavaScript, submitting navigates away to WordPress.com’s “You’ve got mail!” interstitial. With a little JavaScript you can keep the reader on your site and render your own states instead: success (“check your inbox”) when the request lands, error when the network fails. The trick is intercepting the submit and firing the same GET with fetch(url, { mode: 'no-cors' }) — the response is opaque (you can’t read it cross-origin), but you don’t need to: once the request completes, the pending subscriber is created and the confirmation email is on its way.
All three ways of making that request were tested against a live site, and each one creates the pending subscriber: a plain form submit (the no-JavaScript path), a hidden-iframe navigation, and fetch(url, { mode: 'no-cors' }). The response is opaque whichever you pick, so the choice is about your markup, not about what the platform accepts.
const form = document.querySelector('.newsletter-signup');const input = form.querySelector('input[type="email"]');const button = form.querySelector('.newsletter-signup__submit');form.addEventListener('submit', (event) => { event.preventDefault(); if (!input.checkValidity()) { input.reportValidity(); return; } button.disabled = true; button.setAttribute('aria-busy', 'true'); const url = form.action + '?' + new URLSearchParams(new FormData(form)).toString(); fetch(url, { mode: 'no-cors' }) .then(() => show('success', 'Check your inbox! We sent a confirmation link to ' + input.value + '.')) .catch(() => show('error', 'Something went wrong sending that. Please try again.')) .finally(() => { button.disabled = false; button.setAttribute('aria-busy', 'false'); });});function show(type, text) { let el = form.querySelector('.newsletter-status'); if (!el) { el = document.createElement('div'); el.setAttribute('role', 'status'); // announced by screen readers form.appendChild(el); } el.className = 'newsletter-status newsletter-status--' + type; el.textContent = text;}
Details worth keeping:
show()creates the status element on demand and setsrole="status"so screen readers announce the result.- Clear the form on success and keep the input on error — readers shouldn’t retype their address to retry.
input.checkValidity()+reportValidity()keeps the browser’s native email validation working even though we bypassed the native submit.- Style the two states with your own tokens — tinted background, colored left border, whatever matches your design.
One honest limitation: because the response is opaque, a server-side rejection (a spam-flagged address, for example) is indistinguishable from success. The success state is optimistic for well-formed emails — the confirmation email itself is where your reader learns the real outcome.
The one detail you must get right: the blog ID
The blog parameter must be the WordPress.com platform blog ID — which is not always what your PHP thinks it is:
- WordPress.com Simple sites — there’s no sandbox copy; the front-end runs in the platform’s context, so
get_current_blog_id()is the platform ID. Use it directly. - WordPress.com Atomic sites — the front-end PHP runs on a sandboxed copy of the site where
get_current_blog_id()returns1, not the real platform ID. Use the Jetpack connection ID instead:Jetpack_Options::get_option( 'id' ), which returns the platform ID. - Self-hosted WordPress with Jetpack connected — same as Atomic: use
Jetpack_Options::get_option( 'id' ), which is what the Jetpack block itself passes to the iframe.
$blog_id = (int) \Jetpack_Options::get_option( 'id' );if ( ! $blog_id || 1 === $blog_id ) { // fall back to a filter / known constant}
Be careful here: a wrong blog ID fails silently. If the ID belongs to a real site, WordPress.com shows the same “You’ve got mail!” success page — and subscribes the address to that other site. A nonexistent ID just renders an empty “Subscription Management” page. There’s no “wrong site” warning anywhere, so double-check the ID before shipping.
Which setups does this work on?
The endpoint keys on the WordPress.com platform blog ID, so this works for any site whose subscriptions are managed by WordPress.com via the Jetpack Subscriptions/Newsletter module: WordPress.com Simple, WordPress.com Atomic, and self-hosted WordPress with Jetpack connected. It does not work for self-hosted sites without Jetpack, and the Subscriptions module must be enabled.
Where the reader lands after confirming
The link in the confirmation email doesn’t go straight to your site. It goes through WordPress.com’s click tracker, then a magic-login hop, and ends at something like:
https://subscribe.wordpress.com/?key={key}&email={address}&activate={activate}
&redirect_to_blog_post_id={post_id}&source={app_source}
That penultimate parameter decides where the reader ends up. redirect_to_blog_post_id is the post_id your subscribe request carried — the same parameter Jetpack’s own Subscribe block fills in with the post the block sits on. Send nothing and it’s 0, which means the blog index: no “you’re confirmed”, no word that they’re subscribed, no next step. For a reader who already has a WordPress.com account that’s survivable — they land somewhere with the subscription listed. For a fresh reader with no account it’s a dead end, which is exactly the reader your footer form is built for.
So send your own post_id: the id of a page you write for the occasion. A short “you’re in” page that says what just happened and what arrives next beats dumping a new subscriber on the blog index. Pages are posts in WordPress, so a page id works exactly like a post id.
<input type="hidden" name="post_id" value="{landing page id}">
Measured on a live WordPress.com Atomic site (Sept 2026): with post_id in the request, the activation URL carries redirect_to_blog_post_id=416 and the confirming reader lands on that page; without it, 0 and the blog index. Your other request parameters ride along too — that same activation URL carried source=quiz-signup, i.e. the app_source the subscribe request sent.
- Send it as a server-rendered hidden field, so the no-JavaScript submit carries it along with the in-page one.
- Resolve the landing page by slug (or a filter/option) rather than hardcoding an id: pages get recreated, and the id moves with them.
- Write the page for the moment it’s read. “You’re in — here’s what arrives, and here’s how to leave” is the whole job; nothing needs selling twice.
Caveats
- The endpoint is undocumented — it’s what the popup iframe hits. It could change.
- The subscriber’s email travels in the GET query string.
- The JS status enhancement is progressive: with JavaScript off, the submit still works — it navigates to WordPress.com’s branded interstitial instead of your in-page states.
- With JavaScript, server-side rejections can’t be read cross-origin, so the success state is optimistic.
- Where the confirmation link lands is yours to set: send
post_idand WordPress.com carries it through asredirect_to_blog_post_id. It still signs the reader into a WordPress.com account on the way — what you control is the page they see afterwards. - WordPress.com rate-limits this endpoint: per site (“This site has received too many subscription requests. Please try again in a few minutes.”), and separately for addresses carrying too many pending confirmations. Inside such a window every transport above silently does nothing while your UI reports success — so when you’re testing repeatedly, verify against the subscriber list rather than the page (Jetpack’s Subscriptions screen, or
/wp-json/wpcom/v2/subscribers/list). - The URL in the email is a click tracker wrapping the activation URL (
public-api.wordpress.com/bar/?…&_e=<base64>), so debugging a confirmation means reading the redirect chain, not the visible link.