---
title: "Custom Form Integration"
description: "How to build a custom registration form that submits to the B2B Registration Form & Approval endpoint. Covers the form config metafield, field names, submission, and a complete Liquid example."
lastUpdated: 2026-04-16
canonical: https://culsin.com/docs/b2b-registration/custom-form/
source: https://culsin.com/docs/b2b-registration/custom-form/
---
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:**

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

**Via Storefront API:**

```javascript
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`:

```typescript
{
  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

| Type | Input | Notes |
|---|---|---|
| `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 |
| `name` | Two inputs | Submit as `first_name` + `last_name` |
| `address` | Three inputs | Submit as `address` + `city` + `postal_code` |

---

## Field names

| Field | `name` attribute |
|---|---|
| Email | `email` |
| Company name | `company_name` |
| First name | `first_name` |
| Last name | `last_name` |
| Country | `country_code` (ISO 3166-1 alpha-2, e.g. `DE`) |
| VAT / Tax ID | `vat_id` |
| Phone | `phone` |
| Street address | `address` |
| City | `city` |
| Postal code | `postal_code` |
| Custom fields | Use `field.key` from the metafield config |

`email` and `company_name` are always required regardless of field config.

---

## Handling the response

| Status | Body | Meaning |
|---|---|---|
| 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

```liquid
{% 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 %}
```
