HubSpot's Marketplace review process has evolved significantly. What used to be a slow, manual cycle taking one to two weeks per round is now substantially faster — with what feels like meaningful automation behind the scenes. The internal mechanics aren't public, but the result is the same: your submission either meets the requirements or it doesn't.
What hasn't changed are the requirements themselves. The technical standards for upload validity, the quality bar for editor experience, the accessibility baseline — these apply whether a human or an automated system does the checking. This guide covers all of them, based on direct experience publishing multiple modules and themes on the Marketplace.
What the Submission Process Looks Like
After submitting a listing, your asset goes through a review pass. The timeline has compressed noticeably in recent iterations — turnaround that previously took the better part of two weeks now often comes back much faster. Rejection notifications arrive by email, listing the issues found. They're brief — usually a single sentence per item — and there's no direct communication channel with whoever or whatever flagged them.
Two things remain true regardless of how automated the process becomes:
Rejections are not exhaustive. A rejection lists issues found in that specific pass. A second submission after fixing the first round can surface new issues. Keep a running log across all rounds.
The demo page is the primary review surface. Everything that gets evaluated is evaluated on your published demo URL. Source files matter for upload validity — but the live demo is what gets approved or rejected.
Pre-Upload: Technical Requirements That Block Submission
These issues prevent the asset from reaching review at all. The CLI may or may not surface them as hard errors — silent failures are common.
meta.json: The Most Common Upload Failure
Every module needs a valid meta.json. The field that causes the most silent upload failures is host_template_types.
Common mistake:
{
"host_template_types": ["BLOG", "LANDING_PAGE"]
}
Neither BLOG nor LANDING_PAGE are valid enum values. The CLI may not throw a hard error, but the module will fail internal HubSpot validation and may not appear correctly in the editor.
Correct structure:
{
"label": "My Module",
"host_template_types": ["PAGE", "BLOG_POST"],
"is_available_for_new_content": true,
"global": false,
"smart_type": "NOT_SMART",
"tags": []
}
is_available_for_new_content: true is required for the module to show in the editor's module panel. Setting it to false causes the module to upload successfully but be completely invisible to editors — a confusing failure mode because the CLI reports success and no error is thrown anywhere.
fields.json Validation Gotchas
The CLI validates fields.json syntax on upload, but certain issues pass CLI validation and fail silently at runtime.
Hyphens in field id or name values. HubSpot silently rejects field names containing hyphens, even though the CLI accepts the upload without complaint. The module uploads, but the affected fields simply don't render in the editor.
// Will fail silently — avoid
{ "id": "button-label", "name": "button-label", "type": "text" }
// Correct
{ "id": "button_label", "name": "button_label", "type": "text" }
JSON syntax issues that pass standard linting. If fields stop rendering after an upload, validate the file directly:
python3 -m json.tool fields.json
This catches trailing commas, stray characters, and encoding issues. If the file parses cleanly but fields still don't appear, check for a BOM (byte order mark) at the start of the file — some editors insert it invisibly, and HubSpot's parser rejects it without any visible error.
id and name divergence on published modules. The id is what HubSpot uses to map saved editor data. The name is what you reference in HubL as module.field_name. They should always be identical. If you rename the id on a module that's already live on pages, every instance loses the field's saved value — permanently and without warning. Treat id as immutable from the moment any page uses the module.
For a complete breakdown of fields.json field types, defaults, and non-obvious behaviors, see HubSpot fields.json Deep Dive.
Global Partials vs. Modules
A global partial is a template-level file shared structurally across pages — headers, footers, navigation. It lives in templates/ and is referenced via {% include %}.
A module lives in modules/ as a .module directory, is editor-placeable, and is referenced via {% module %} or placed via drag-and-drop in the page editor.
The failure mode: placing a partial-style file in modules/, or using {% include %} paths that resolve incorrectly after a CLI upload. The result is a rendering error that only appears on the live page — not in CLI output, not in Design Manager preview.
The rule: if an editor needs to place it on a page, it's a module. If it's structural scaffolding shared at the developer level across templates, it's a partial.
What Gets Flagged During Review
The following patterns account for the majority of rejections across module and theme submissions.
1. Missing Default Content
The module is placed on a blank page with zero configuration and evaluated as-is.
The standard: a developer who installs your module and places it without touching any editor fields should see a complete, presentable component — not empty boxes, not layout without content, not placeholder strings like "Your title here."
Every visible output element needs a meaningful default in fields.json:
{
"id": "headline",
"name": "headline",
"label": "Headline",
"type": "text",
"default": "Why Every Team Needs a Smarter Onboarding Process"
}
Image field defaults must include a real src and descriptive alt:
{
"id": "hero_image",
"name": "hero_image",
"label": "Image",
"type": "image",
"default": {
"src": "https://yourcdn.com/demo-image.jpg",
"alt": "Team collaborating around a whiteboard",
"width": 1200,
"height": 630,
"loading": "lazy"
}
}
For repeater groups: the default array must contain at least one complete item with real content. Note that repeater defaults have reliability nuances — the safest approach combines fields.json defaults with a server-side demo fallback in HubL. Full mechanics covered in Why You Can't Prefill Complex HubSpot Modules.
2. Hardcoded Content That Should Be Editable
The inverse of the above. If your module renders a headline, button label, color, icon, or URL — and there's no corresponding field in fields.json — it gets flagged.
The test: imagine your module deployed in a client portal with no developer available. Can they change everything they might reasonably need to change, without touching code?
Hardcoded values are acceptable only for genuinely structural properties: CSS class names, schema markup, semantic HTML attributes, ARIA roles. Everything visible and content-bearing should map to an editable field.
3. Accessibility — The Quiet Dealbreaker
Accessibility failures are the hardest to anticipate because they require active testing, not just reading source. The three most common items:
Missing alt text on images. Every <img> must have an alt attribute. Decorative images use alt="". Content-bearing images need descriptive text. The template must output the alt value from the field — not hardcode it, not omit it:
<img
src="{{ module.hero_image.src }}"
alt="{{ module.hero_image.alt }}"
width="{{ module.hero_image.width }}"
height="{{ module.hero_image.height }}"
loading="{{ module.hero_image.loading }}"
>
Interactive elements not reachable by keyboard. Tabs, accordions, carousels, dropdowns — all must respond to Tab, Enter, Space, and where applicable, Arrow keys. Using native <button> elements instead of <div> with click handlers solves most of this automatically. A <div> with an onclick attribute is not keyboard accessible by default and will be flagged.
Missing ARIA on stateful components. Components that change visible state require ARIA attributes that reflect that state in real time:
<button
id="trigger-1"
aria-expanded="false"
aria-controls="panel-1"
class="accordion__trigger"
>
Section Title
</button>
<div
id="panel-1"
role="region"
aria-labelledby="trigger-1"
hidden
>
Content here
</div>
When the panel opens, aria-expanded must update to "true" and hidden must be removed via JavaScript. Without this, screen reader users receive no indication that the component changed state.
Full ARIA implementation patterns for tabs and accordions: Mastering ARIA: Enhancing Accessibility for Interactive Interfaces.
4. Console Errors on the Demo Page
Open the browser DevTools console on your published demo URL before every submission. JavaScript errors are caught during review. Common sources:
querySelector returning null when no editor content is configured:
// Throws TypeError if element doesn't render without editor content
const slider = document.querySelector('.my-slider');
slider.addEventListener('input', handler);
// Safe
const slider = document.querySelector('.my-slider');
if (!slider) return;
slider.addEventListener('input', handler);
Undefined property access on HubL variables. Guard against fields that may be empty:
{% if module.items and module.items|length > 0 %}
...
{% endif %}
Missing external script dependencies. Any library your module relies on must either be loaded by the module itself or clearly documented as a prerequisite. Don't assume the portal has jQuery, Alpine, or any other library present.
5. Responsive Layout
The demo is evaluated at multiple viewport widths. Common failure modes:
- Fixed-width containers that overflow and cause horizontal scroll on mobile
- Images without
max-width: 100%that break out of their containers - Buttons or interactive elements with tap targets smaller than 44×44px
- Text that truncates or wraps into unreadable layouts at narrow widths
Test at 375px (mobile), 768px (tablet), and 1280px (desktop) before every submission. Horizontal scroll on mobile is an immediate rejection.
6. Field Labels and Help Text
Every field in fields.json must have a human-readable label. Not "field_1", not the raw name value. The editor sidebar experience is evaluated, not just the front-end output.
Fields with non-obvious behavior must have help_text:
{
"id": "column_count",
"name": "column_count",
"label": "Columns per row",
"type": "choice",
"choices": [["2", "2 columns"], ["3", "3 columns"], ["4", "4 columns"]],
"default": "3",
"help_text": "Controls how many items appear per row on desktop. On mobile, content always stacks to a single column regardless of this setting."
}
7. Required Template Types for Themes
For theme submissions, the template set must cover the minimum required types. Missing any of the following results in rejection regardless of visual quality:
- Home page template
- Standard page template
- Blog listing template
- Blog post template
- System error page template (404)
Every template must reference only CSS and JS files that actually exist within the theme directory. Broken asset references — even in development-only files that weren't cleaned up — cause failures.
Pre-Submission Checklist
Run this before every submission, against the live demo page — not a local environment.
Upload validity
host_template_types usesonly valid values:PAGE,BLOG_POST,BLOG_LISTING,EMAIL,KNOWLEDGE_ARTICLE,QUOTE_TEMPLATEis_available_for_new_content: trueon every editor-placeable modulefields.jsonpassespython3 -m json.tool fields.jsonwith no errors- No hyphens in any field
idorname - No BOM character at the start of JSON files (check if edited on Windows)
idandnameare identical for every field
Content and editor experience
- Every visible output element has a corresponding editable field
- Every field has a clear, human-readable
label - Every non-obvious field has
help_text - Repeater
defaultarray contains at least one complete, real-content item - Image field defaults include a real
srcURL and descriptivealttext - Module renders as a complete, presentable component with zero editor configuration
Accessibility
- All
<img>tags output thealtattribute from the image field - All interactive elements use
<button>or are fully keyboard-accessible - Stateful components update
aria-expanded,aria-selected, or equivalent on interaction - Tab order is logical through the full component
Demo page
- Browser console is clear of JavaScript errors on the published demo URL
- Layout tested at 375px, 768px, and 1280px viewport widths
- No horizontal scroll at any viewport width
- All interactions function correctly without errors
Themes only
- Home, page, blog listing, blog post, and 404 error templates all present
- All templates reference only assets that exist within the theme directory
After a Rejection
Read all listed items before touching anything. Some fixes address multiple rejection items at once. Understanding the full list before acting prevents redundant work and missed connections between issues.
Fix every listed item before resubmitting. Partial fixes waste a round. The next pass will find the unfixed items, and potentially surface new ones on top.
Republish the demo page after fixing. HubSpot caches published content aggressively. Uploading a fixed module source does not automatically update live pages. Republish every affected page, verify the fix is live in the browser, then resubmit.
Test for regressions. A fix in one area can introduce a problem in another. After every change, re-run the relevant checklist sections — not only the section covering what you fixed.
The checklist above, applied before every first submission, consistently reduces rejection rounds. The requirements themselves are stable — what changed is how fast you find out whether you've met them.