Shopify B2B — registration & approval

Wholesale applications become real Shopify companies.

A buyer applies. A human reads it. On approval, B2B Triage writes native Shopify objects — Company, location, contact, catalog, payment terms, tax exemption. Not a tag. Not answers pasted into a notes field. The objects Shopify's own B2B engine runs on.

$20/month. Native Shopify B2B is on every paid plan — it stopped being Plus-only in April 2026.

01 — The object graph

One approval. Six native objects.

The common approach ends at a customer record with the answers pasted into a note. B2B Triage runs a pipeline that creates the things Shopify's B2B engine actually reads — the same objects you would get by doing it by hand, without doing it by hand.

01

Company

Created with a stable external ID derived when the buyer applies, not when you approve. Shopify does not enforce unique company names — without that ID, a double-clicked Approve silently creates two.

02

Company location

Including the second one Shopify auto-creates behind your back, carrying no terms, no exemption and no catalog. It is the one buyers pick, and it sells at retail. We detect it and reconcile it.

03

Company contact

The buyer, attached to the location, using the assignment mutation that works when the customer already exists — which is always.

04

Catalog assignment

Their price list, asserted ACTIVE on read-back rather than assumed. Shopify can silently force it to ARCHIVED.

05

Payment terms

Net 30 resolved from the template to a typed integer — never parsed out of the display string, which breaks on the first French store.

06

Tax exemption

The jurisdiction-correct value from Shopify's 75. Canada alone has six kinds. A blanket boolean is not the same thing.

Nine steps, each one recorded on the application's own timeline. When Shopify refuses a step, you get Shopify's own words — not an error code we invented.

02 — Ownership

Cancel us. Keep everything.

Every app wants your data. Almost none will tell you what happens to it the day you leave. Here is our answer, and it is checkable.

Shopify records no app-ownership link on a Company. Metafields have a reserved $app: namespace, and an uninstall does sever those. Companies have no such field — nothing ties a Company, a location, a contact, a catalog assignment, payment terms or a tax exemption to the app that created it. There is nothing for an uninstall to take away.

We did not reason that out and hope. B2B Triage was installed on a live store. We captured the state with a credential that has nothing to do with the app, uninstalled B2B Triage, and ran the identical query again.

Object Before uninstall After uninstall
Companies on the store 3 3
Company + external ID present present, identical
Contacts / locations 1 / 2 1 / 2
Payment terms Net 30 (NET, dueInDays 30) Net 30 (NET, dueInDays 30)
Tax exemption US_CO_RESELLER_EXEMPTION US_CO_RESELLER_EXEMPTION
Catalog ACTIVE ACTIVE
Byte-for-byte identical. Your companies belong to your store, not to your subscription.

Stated precisely, because it is the kind of claim that deserves it: what is proven is the mechanism — Shopify has no app-ownership link on these objects, so there is nothing to sever, whoever created them. The objects in the run above were written with an independent Admin API credential rather than the app's own token. The result is the same for the same reason, and we would rather show you the seam than sand it down.

03 — Your data

Structured at input, so the export is CRM-ready.

We build apps that structure your data and track ROI. That is the whole product, and it shows up in an unglamorous place: where the answers go.

The common approach stores registration answers as a delimited plaintext blob inside the customer's Notes field. It works, in the sense that a human can read it. But a note is free text: any staff member can edit it or wipe it, it collides with whatever else your team uses that field for, and no report, segment or query can reach inside it. The data is on the record without being data.

B2B Triage writes typed, namespaced metafields. A number is a number.

Reportable
Metafields reach ShopifyQL as a GROUP BY dimension. Tags have no analytics dimension at all — you cannot report on a tag.
Typed
net_terms_days = 30 supports "greater than 30". The tag net30 string-collides with net300 and supports nothing.
Namespaced
Everything lands under b2b_triage. Tags are one flat global namespace shared with every app on your store — two apps writing "wholesale" collide in silence.
Queryable
"Every approved buyer whose VAT number no registry ever confirmed" is a ShopifyQL query, not an afternoon. (Shopify does not offer admin-list filtering on customer metafields — the answer comes from analytics and the export, not the customer list.)
Exportable
CSV with Shopify GIDs as join keys, so the row matches your CRM instead of approximating it. Values are normalised at input — the export is clean because the data was never dirty.

We write tags as well — but as a compatibility bridge, for tools that cannot read metafields (Klaviyo's native Shopify sync reads tags and not metafields). The metafields are the truth. The tags are a courtesy.

04 — Tax IDs

The 21 minutes Google Ireland was a fraud.

VIES is the EU's VAT registry. It answers HTTP 200 when it is broken, and reports its own outages as isValid: false. The actual reason hides in a different field.

On 16 July 2026 we asked it about IE6388047V — Google Ireland's real, live VAT number. For 21 minutes it came back invalid, because Ireland's member-state service was down. Then, without anything changing at Google, it came back valid.

Time (UTC) isValid userError The naive reading
05:14 false MS_UNAVAILABLE "Invalid" — fraud-flagged
05:35 true VALID GOOGLE IRELAND LIMITED

The integration everyone writes first — return isValid ? "valid" : "invalid" — permanently marks Google Ireland as a fraud, silently, with a 200 OK, and nobody ever finds out.

We never read isValid. We read the error code, as an allowlist: exactly one value means "not registered". Everything else — including codes the EC ships next year — means "we could not check right now", which is a different sentence and gets a different word on screen.

And we never reject anyone because a government registry blinked. A wholesale application is a revenue event. The registry informs you; you decide. There is no branch in our code that turns a registry's bad day into a rejection — not a policy we promise, a branch that does not exist.

At any given moment some member state is failing. That is the normal operating condition, not an incident. "Never checked", "checked and found nothing" and "the check failed" are three different facts, and they get three different answers. A boolean would collapse them into a lie.

04b — Coverage, precisely

"Validated" and "checked against the registry" are not the same word.

So we use both, and we tell you which one you got.

Validated offline — format and check digit
EU VAT, UK VAT, Australian ABN, India GST, US EIN. Real arithmetic, not a regex. This runs first and gates everything: a value we can already prove is a typo never reaches a government registry.
Checked against the live registry
EU VAT, via VIES. Today. That is the whole list, and we would rather say so than imply five.
Built, waiting on a credential
Australia's ABR. The client is written and tested; we do not hold the GUID yet, so no call has returned real ABN data. Until it does, the answer is "we never asked" — not a checkmark.
Not built
UK HMRC needs an OAuth credential (about two weeks). We return "unsupported" rather than shipping a client that 401s on every call and reports it as the buyer's problem.
Impossible, not missing
India and the US have no public registry to check. The IRS's TIN matching is 1099-filers only; India's GSTN needs GSP empanelment. Nobody can do this, and anyone charging you for it is checking the format.
05 — Forms

(A and B) or C.

Conditional logic is usually the thing you upgrade for. Ours does nested AND/OR groups — not a single global "match all / match any" switch, which by construction cannot express that line above.

Show the resale certificate field when the country is Canada and the province is Québec — or whenever they picked "distributor", wherever they are. That is one rule, and it is $20.

Hidden means absent, and it is enforced on our side. A hidden field contributes no value. A hidden required field does not block submission. Hidden values are never trusted: the server re-evaluates every condition before it accepts anything, so a crafted POST cannot smuggle a value your logic never showed. The same module runs the editor preview and the submission — one implementation, because two would drift.

06 — Input quality

Normalise. Don't reject.

The buyer typed something they believe is correct. If we can fix it, we fix it, and we show them what we stored. Every avoidable friction here is a lost order.

  • h9b2g9 becomes H9B 2G9. Per country — a US ZIP is not a Canadian postal code. Rejecting it for a missing space is us failing, not them.
  • Phone numbers store as E.164. Letters, brackets, spaces, a leading trunk zero: noise, stripped.
  • Formatting happens on blur, never on keystroke. Nothing reformats under your cursor — the single most hated behaviour a form has.
  • 244 countries. Province and state dropdowns for every country that has them, not just the two you would expect — built from Shopify's own zone data, which is not ISO 3166-2. Mexico's Aguascalientes is AGS to Shopify and AGU to ISO, and Shopify accepts the wrong one without an error.
  • Errors diagnose instead of grade. "Enter a valid email address" is a verdict. "That address is missing everything after the @" is a fix. The word "invalid" never appears — a test enforces it.
  • 16px inputs so iOS Safari does not zoom on focus. Real labels, errors announced to screen readers, 44px touch targets, correct autocomplete attributes.
  • Long option lists stop being a dropdown past about a hundred entries and become a searchable field, answered by the server as the buyer types. A 5,000-option select is unusable and would blow the storefront budget on its own.
07 — Your storefront

Zero third-party requests. Works with JavaScript off.

Your storefront's speed is your SEO and your conversion rate. An app that taxes every page to serve one form is not free — you just do not get the invoice.

  • No third-party requests at all. No CAPTCHA, no tracker, no CDN font, no analytics pixel. Nothing on your storefront phones anywhere but your storefront.
  • It loads only where you place it. It is a theme app block, not an app embed — an embed runs on every page of your store, including the ones with no form on them.
  • Under 13 KB gzipped, measured from the built artifact by a build step that fails when it grows, not estimated by us.
  • The form works with JavaScript disabled. It is a real form that posts to a real endpoint, enhanced when JS is available, functional when it is not.
  • Any third-party lookup we do — a tax registry, an address service — runs on our server, never in your buyer's browser. Our keys stay ours, and your storefront's request count stays at zero.
08 — Pricing

Two plans. That's the whole page.

Billed by Shopify, on your existing invoice.

B2B Triage

$20/month

Everything above. All of it.

  • Registration forms — multi-step, custom fields, conditional AND/OR logic
  • Approval queue — manual, automatic, or auto-approve on an email domain
  • Native Company, location, contact, catalog, terms, tax exemption on approval
  • Typed metafields + tags for compatibility
  • Tax ID validation, and EU VAT checked live against VIES
  • CSV export with Shopify GIDs as join keys
  • Shopify Flow trigger carrying typed application data

B2B Triage + RetailNot built yet

$35/month

Everything in the $20 plan, plus the below — which are not built yet.

  • Point of sale — B2B prices at the counter
  • Public API
  • Klaviyo integration

In development, and listed here so the roadmap is not a secret. Do not buy this plan for these three features today — buy the $20 plan, and we will tell you when this one is real.

Native Shopify B2B is available on every paid Shopify plan — it has not been Plus-only since April 2026. A few pieces of Shopify's own B2B are still Plus-gated, and we are specific about which in the FAQ.

09 — Questions

The questions worth answering.

What exactly does B2B Triage create when I approve someone?

Native Shopify B2B objects: a Company, a CompanyLocation, a CompanyContact, a catalog assignment, payment terms and a jurisdiction-correct tax exemption — plus typed metafields on the customer and tags for tools that only read tags. These are ordinary Shopify records. Your staff can edit them in the admin, your other apps can read them, and Shopify's B2B engine uses them directly, because they are its own.

Do I need Shopify Plus?

No. Shopify's native B2B has been available on every paid plan since April 2026 — the "B2B is Plus-only" rule is real history and out of date. Three things are still Plus-gated by Shopify, not by us: assigning a catalog directly to a company, having more than three catalogs (non-Plus caps at three), and partial payments or deposits. Everything else in the list above works on a paid non-Plus plan. Straight answer on our side: our own store is Plus, and we have not yet watched a non-Plus install fail or pass. If you are on a non-Plus plan and something is wrong, we want to be the ones who find out — write to us.

What happens to my companies if I uninstall?

Nothing. They stay. Shopify records no app-ownership link on a Company, a location, a contact, a catalog assignment, payment terms or a tax exemption — unlike metafields, which live under a reserved $app: namespace that an uninstall does sever. There is nothing for our removal to take away. We tested it on a live store and the objects came back byte-for-byte identical, down to "Net 30, dueInDays 30" and a catalog still marked ACTIVE. You are not renting your customer data back from us.

How do you handle spam?

A honeypot field, a rate limit by IP, and the approval queue — which is the real backstop, because no application becomes a company until a human reads it. We do not ship a CAPTCHA. We had one, it caused more problems than it solved, and we removed it rather than leave a control that lies. So: spam can cost you attention and a database row. It cannot cost you money, and it cannot create anything on your store. If a CAPTCHA is a hard requirement for you, that is a real gap and we would rather you knew now.

Is my buyers' data safe? Where does it go?

It stays in your Shopify store and in our database, and nowhere else. Tax IDs and VAT numbers are marked as private to the storefront — they are never readable from your theme. We put no PII in logs and none in URLs. Applicant personal data is anonymised automatically 730 days after the application is decided, on a schedule, without anyone remembering to do it. Approved companies are never touched by that — they are your records, not ours.

Can I migrate from another wholesale app?

That is the plan, and we will be straight about its status: the importer reads a CSV, including registration answers stored as a plaintext blob in the customer's Notes field by the common approach — and creates the native Companies that were never made. Mapping rules (Québec becomes Net 30; assign this sales rep) and filters come with it. It is written and not yet finished. Ask us where it stands before you count on it.

Does it slow my store down?

It loads only on the page where you place the block, it makes no third-party requests, and it is under 13 KB gzipped — a number a build step enforces rather than a number we like. There is no sitewide script. An app that injects itself into every page of your store to serve one form is charging your whole storefront for one page.

Do you email my buyers?

The approved-buyer invitation is Shopify's own account invite — their sender, their template, nothing of ours in the middle. That only exists on legacy customer accounts; if you are on New Customer Accounts, Shopify does not send it, we do not fake it, and the step says so in your admin rather than pretending. We also send a submission confirmation and a decline notice from our own address, with your address as the reply-to. Honest status: that transport is new and no message has yet reached a real inbox. It is built. It is not proven, and we will not call it proven.

Who builds this?

Human After All, a Shopify agency in Montréal. We build B2B on Shopify for a living and made this because the tools we needed either did not exist or created a parallel system beside the store instead of using the store. If you want to argue with an engineering decision on this page, you will get an engineer.

10 — Contact

Ask us something specific.

A real person reads these. If you have a question about a claim on this page, ask it — we would rather be corrected than believed.

Optional — your myshopify.com domain or your storefront

We use what you send to reply to you. Nothing else, and nobody else.