Custom Form Integration

Last updated: April 16, 2026

The app ships a B2B Registration Form block you add via the theme editor. If you need full control over markup and UX, you can build your own form instead and POST to the same endpoint.


Submission endpoint

POST /apps/b2b/register

Accepts application/x-www-form-urlencoded or multipart/form-data. Returns JSON.


Reading the form config

The app publishes a PUBLIC_READ shop metafield (b2b_app.form_config) with the active field schema. Read it to know which fields are enabled and their configuration.

In Liquid:

{% assign cfg = shop.metafields.b2b_app.form_config.value %}

Via Storefront API:

const { data } = await fetch('/api/2024-01/graphql.json', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-Shopify-Storefront-Access-Token': window.Shopify.storefrontAccessToken,
  },
  body: JSON.stringify({ query: `{
    shop {
      metafield(namespace: "b2b_app", key: "form_config") { value }
    }
  }` }),
}).then(r => r.json());

const cfg = JSON.parse(data.shop.metafield.value);

Field schema

Each object in cfg.fields:

{
  key: string;          // POST field name
  type: string;         // see field types below
  label: string;
  placeholder: string;
  required: boolean;
  options?: { value: string; label: string }[];   // select fields
  countries?: { code: string; name: string }[];   // country fields
}

Field types

TypeInputNotes
text<input type="text">
email<input type="email">
tel<input type="tel">
number<input type="number">
url<input type="url">
textarea<textarea>
select<select>Options in field.options
checkbox<input type="checkbox">Submitted as "on" when checked
country<select>Options in field.countries; submit the country code
vat<input type="text">Server validates format and optionally checks VIES
nameTwo inputsSubmit as first_name + last_name
addressThree inputsSubmit as address + city + postal_code

Field names

Fieldname attribute
Emailemail
Company namecompany_name
First namefirst_name
Last namelast_name
Countrycountry_code (ISO 3166-1 alpha-2, e.g. DE)
VAT / Tax IDvat_id
Phonephone
Street addressaddress
Citycity
Postal codepostal_code
Custom fieldsUse field.key from the metafield config

email and company_name are always required regardless of field config.


Handling the response

StatusBodyMeaning
200{ ok: true }Submitted. Also returned for duplicate submissions (idempotent).
400{ error: "..." }Missing or invalid field
403{ error: "Registration form is not enabled" }Form disabled in settings
422{ error: "VAT / Tax ID format is not valid for this country." }VAT format check failed
429{ error: "Your store has reached its application limit. Please upgrade your plan to continue accepting applications." }Plan application limit reached

Display result.error directly - the messages are user-readable.


Complete Liquid example

{% assign cfg = shop.metafields.b2b_app.form_config.value %}

{% if cfg == blank %}
  <p>Form not configured yet.</p>
{% elsif customer and customer.tags contains 'b2b-accepted' %}
  <p>You already have an active B2B account.</p>
{% elsif customer and customer.tags contains 'b2b-pending' %}
  <p>Your application is under review.</p>
{% else %}

<div id="b2b-wrap">
  <div id="b2b-success" style="display:none">
    <p>Application received. We'll be in touch.</p>
  </div>

  <form id="b2b-form" novalidate>
    {% for field in cfg.fields %}
      {% if field.type == 'name' %}
        <label>First name
          <input type="text" name="first_name" {% if field.required %}required{% endif %}>
        </label>
        <label>Last name
          <input type="text" name="last_name" {% if field.required %}required{% endif %}>
        </label>

      {% elsif field.type == 'address' %}
        <label>{{ field.label }}
          <input type="text" name="address" {% if field.required %}required{% endif %}>
        </label>
        <label>City
          <input type="text" name="city" {% if field.required %}required{% endif %}>
        </label>
        <label>Postal code
          <input type="text" name="postal_code" {% if field.required %}required{% endif %}>
        </label>

      {% elsif field.type == 'country' %}
        <label>{{ field.label }}
          <select name="country_code" {% if field.required %}required{% endif %}>
            <option value=""></option>
            {% for c in field.countries %}
              <option value="{{ c.code }}">{{ c.name }}</option>
            {% endfor %}
          </select>
        </label>

      {% elsif field.type == 'select' %}
        <label>{{ field.label }}
          <select name="{{ field.key }}" {% if field.required %}required{% endif %}>
            <option value=""></option>
            {% for opt in field.options %}
              <option value="{{ opt.value }}">{{ opt.label }}</option>
            {% endfor %}
          </select>
        </label>

      {% elsif field.type == 'textarea' %}
        <label>{{ field.label }}
          <textarea name="{{ field.key }}" placeholder="{{ field.placeholder }}" {% if field.required %}required{% endif %}></textarea>
        </label>

      {% elsif field.type == 'checkbox' %}
        <label>
          <input type="checkbox" name="{{ field.key }}" {% if field.required %}required{% endif %}>
          {{ field.label }}
        </label>

      {% else %}
        <label>{{ field.label }}
          <input type="{{ field.type | replace: 'vat', 'text' }}" name="{{ field.key }}"
            placeholder="{{ field.placeholder }}" {% if field.required %}required{% endif %}>
        </label>
      {% endif %}
    {% endfor %}

    <p id="b2b-error" style="display:none; color:red;"></p>
    <button type="submit">Apply for wholesale access</button>
  </form>
</div>

<script>
(function () {
  var wrap = document.getElementById('b2b-wrap');
  var form = document.getElementById('b2b-form');
  var errorEl = document.getElementById('b2b-error');
  var successEl = document.getElementById('b2b-success');

  form.addEventListener('submit', async function (e) {
    e.preventDefault();
    errorEl.style.display = 'none';

    try {
      var res = await fetch('/apps/b2b/register', { method: 'POST', body: new FormData(form) });
      var data = await res.json();
      if (data.ok) {
        form.style.display = 'none';
        successEl.style.display = '';
      } else {
        errorEl.textContent = data.error || 'Something went wrong.';
        errorEl.style.display = '';
      }
    } catch (_) {
      errorEl.textContent = 'Network error. Please try again.';
      errorEl.style.display = '';
    }
  });
})();
</script>

{% endif %}