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.
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.
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.
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.
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.
The buyer, attached to the location, using the assignment mutation that works when the customer already exists — which is always.
Their price list, asserted ACTIVE on read-back rather than assumed. Shopify can silently force it to ARCHIVED.
Net 30 resolved from the template to a typed integer — never parsed out of the display string, which breaks on the first French store.
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.
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 |
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.
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.
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.
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.
So we use both, and we tell you which one you got.
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.
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.
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.
Billed by Shopify, on your existing invoice.
Everything above. All of it.
Everything in the $20 plan, plus the below — which are not built yet.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.